mirror of
https://github.com/standardnotes/app.git
synced 2026-01-11 19:56:41 +00:00
1568 lines
49 KiB
TypeScript
1568 lines
49 KiB
TypeScript
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<void>
|
|
|
|
type ApplicationObserver = {
|
|
singleEvent?: ApplicationEvent
|
|
callback: ApplicationEventCallback
|
|
}
|
|
|
|
type ItemStream<I extends DecryptedItemInterface> = (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<string>
|
|
/**
|
|
* @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<any, any>[] = []
|
|
private streamRemovers: ObserverRemover[] = []
|
|
private serviceObservers: ObserverRemover[] = []
|
|
private managedSubscribers: ObserverRemover[] = []
|
|
private autoSyncInterval!: ReturnType<typeof setInterval>
|
|
|
|
/** 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<string | undefined> {
|
|
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<void> {
|
|
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<void> {
|
|
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<DiagnosticInfo> {
|
|
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<Responses.HttpResponse | undefined> {
|
|
if (await this.protectionService.authorizeSessionRevoking()) {
|
|
return this.sessionManager.revokeSession(sessionId)
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
/**
|
|
* Revokes all sessions except the current one.
|
|
*/
|
|
public async revokeAllOtherSessions(): Promise<void> {
|
|
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<Subscription | Responses.ClientDisplayableError> {
|
|
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<I extends DecryptedItemInterface = DecryptedItemInterface>(
|
|
contentType: Common.ContentType | Common.ContentType[],
|
|
stream: ItemStream<I>,
|
|
): () => void {
|
|
const removeItemManagerObserver = this.itemManager.addObserver<I>(
|
|
contentType,
|
|
({ changed, inserted, removed, source }) => {
|
|
stream({ changed, inserted, removed, source })
|
|
},
|
|
)
|
|
|
|
const matches = this.itemManager.getItems<I>(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<void> {
|
|
this.httpService.setHost(host)
|
|
|
|
await this.apiService.setHost(host)
|
|
}
|
|
|
|
public getHost(): string | undefined {
|
|
return this.apiService.getHost()
|
|
}
|
|
|
|
public async setCustomHost(host: string): Promise<void> {
|
|
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<string | undefined> {
|
|
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<boolean> {
|
|
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<void> {
|
|
return this.protectionService.clearSession()
|
|
}
|
|
|
|
public async authorizeProtectedActionForNotes(
|
|
notes: Models.SNNote[],
|
|
challengeReason: ChallengeReason,
|
|
): Promise<Models.SNNote[]> {
|
|
return await this.protectionService.authorizeProtectedActionForItems(notes, challengeReason)
|
|
}
|
|
|
|
/**
|
|
* @returns whether note access has been granted or not
|
|
*/
|
|
public authorizeNoteAccess(note: Models.SNNote): Promise<boolean> {
|
|
return this.protectionService.authorizeItemAccess(note)
|
|
}
|
|
|
|
public authorizeAutolockIntervalChange(): Promise<boolean> {
|
|
return this.protectionService.authorizeAutolockIntervalChange()
|
|
}
|
|
|
|
public authorizeSearchingProtectedNotesText(): Promise<boolean> {
|
|
return this.protectionService.authorizeSearchingProtectedNotesText()
|
|
}
|
|
|
|
public canRegisterNewListedAccount(): boolean {
|
|
return this.listedService.canRegisterNewListedAccount()
|
|
}
|
|
|
|
public async requestNewListedAccount(): Promise<Responses.ListedAccount | undefined> {
|
|
return this.listedService.requestNewListedAccount()
|
|
}
|
|
|
|
public async getListedAccounts(): Promise<Responses.ListedAccount[]> {
|
|
return this.listedService.getListedAccounts()
|
|
}
|
|
|
|
public getListedAccountInfo(
|
|
account: Responses.ListedAccount,
|
|
inContextOfItem?: UuidString,
|
|
): Promise<Responses.ListedAccountInfo | undefined> {
|
|
return this.listedService.getListedAccountInfo(account, inContextOfItem)
|
|
}
|
|
|
|
public async createEncryptedBackupFileForAutomatedDesktopBackups(): Promise<Encryption.BackupFile | undefined> {
|
|
return this.protocolService.createEncryptedBackupFile()
|
|
}
|
|
|
|
public async createEncryptedBackupFile(): Promise<Encryption.BackupFile | undefined> {
|
|
if (!(await this.protectionService.authorizeBackupCreation())) {
|
|
return
|
|
}
|
|
|
|
return this.protocolService.createEncryptedBackupFile()
|
|
}
|
|
|
|
public async createDecryptedBackupFile(): Promise<Encryption.BackupFile | undefined> {
|
|
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<void> {
|
|
return this.diskStorageService.removeValue(key, mode)
|
|
}
|
|
|
|
public getPreference<K extends Models.PrefKey>(key: K): Models.PrefValue[K] | undefined
|
|
public getPreference<K extends Models.PrefKey>(key: K, defaultValue: Models.PrefValue[K]): Models.PrefValue[K]
|
|
public getPreference<K extends Models.PrefKey>(
|
|
key: K,
|
|
defaultValue?: Models.PrefValue[K],
|
|
): Models.PrefValue[K] | undefined {
|
|
return this.preferencesService.getValue(key, defaultValue)
|
|
}
|
|
|
|
public async setPreference<K extends Models.PrefKey>(key: K, value: Models.PrefValue[K]): Promise<void> {
|
|
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<void> {
|
|
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<ChallengeResponse | undefined> {
|
|
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<void> {
|
|
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<UserRegistrationResponseBody> {
|
|
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<Responses.HttpResponse | Responses.SignInResponse> {
|
|
return this.userService.signIn(email, password, strict, ephemeral, mergeLocal, awaitSync)
|
|
}
|
|
|
|
public async changeEmail(
|
|
newEmail: string,
|
|
currentPassword: string,
|
|
passcode?: string,
|
|
origination = Common.KeyParamsOrigination.EmailChange,
|
|
): Promise<InternalServices.CredentialsChangeFunctionResponse> {
|
|
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<InternalServices.CredentialsChangeFunctionResponse> {
|
|
return this.userService.changeCredentials({
|
|
currentPassword,
|
|
newPassword,
|
|
passcode,
|
|
origination,
|
|
validateNewPasswordStrength,
|
|
})
|
|
}
|
|
|
|
private async handleRevokedSession(): Promise<void> {
|
|
/**
|
|
* 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<boolean> {
|
|
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<boolean> {
|
|
return this.protectionService.disableBiometrics()
|
|
}
|
|
|
|
public hasPasscode(): boolean {
|
|
return this.protocolService.hasPasscode()
|
|
}
|
|
|
|
isLocked(): Promise<boolean> {
|
|
if (!this.started) {
|
|
return Promise.resolve(true)
|
|
}
|
|
return this.challengeService.isPasscodeLocked()
|
|
}
|
|
|
|
public async lock(): Promise<void> {
|
|
/** 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<boolean> {
|
|
return this.userService.addPasscode(passcode)
|
|
}
|
|
|
|
/**
|
|
* @returns whether the passcode was successfuly removed
|
|
*/
|
|
public async removePasscode(): Promise<boolean> {
|
|
return this.userService.removePasscode()
|
|
}
|
|
|
|
public async changePasscode(
|
|
newPasscode: string,
|
|
origination = Common.KeyParamsOrigination.PasscodeChange,
|
|
): Promise<boolean> {
|
|
return this.userService.changePasscode(newPasscode, origination)
|
|
}
|
|
|
|
public getStorageEncryptionPolicy(): ExternalServices.StorageEncryptionPolicy {
|
|
return this.diskStorageService.getStorageEncryptionPolicy()
|
|
}
|
|
|
|
public setStorageEncryptionPolicy(encryptionPolicy: ExternalServices.StorageEncryptionPolicy): Promise<void> {
|
|
this.diskStorageService.setEncryptionPolicy(encryptionPolicy)
|
|
return this.protocolService.repersistAllItems()
|
|
}
|
|
|
|
public enableEphemeralPersistencePolicy(): Promise<void> {
|
|
return this.diskStorageService.setPersistencePolicy(ExternalServices.StoragePersistencePolicies.Ephemeral)
|
|
}
|
|
|
|
public hasPendingMigrations(): Promise<boolean> {
|
|
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<boolean> {
|
|
return this.mfaService.isMfaActivated()
|
|
}
|
|
|
|
public async generateMfaSecret(): Promise<string> {
|
|
return this.mfaService.generateMfaSecret()
|
|
}
|
|
|
|
public async getOtpToken(secret: string): Promise<string> {
|
|
return this.mfaService.getOtpToken(secret)
|
|
}
|
|
|
|
public async enableMfa(secret: string, otpToken: string): Promise<void> {
|
|
return this.mfaService.enableMfa(secret, otpToken)
|
|
}
|
|
|
|
public async disableMfa(): Promise<void> {
|
|
if (await this.protectionService.authorizeMfaDisable()) {
|
|
return this.mfaService.disableMfa()
|
|
}
|
|
}
|
|
|
|
public getNewSubscriptionToken(): Promise<string | undefined> {
|
|
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<typeof InternalServices.SNComponentManager>(
|
|
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<T>(base: T) {
|
|
const swapClass = this.options.swapClasses?.find((candidate) => candidate.swap === base)
|
|
if (swapClass) {
|
|
return swapClass.with as T
|
|
} else {
|
|
return base
|
|
}
|
|
}
|
|
}
|