refactor(web): dependency management (#2386)
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { SNMfaService } from './../Services/Mfa/MfaService'
|
||||
import { MfaService } from './../Services/Mfa/MfaService'
|
||||
import { KeyRecoveryService } from './../Services/KeyRecovery/KeyRecoveryService'
|
||||
import { WebSocketsService } from './../Services/Api/WebsocketsService'
|
||||
import { MigrationService } from './../Services/Migration/MigrationService'
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
ApplicationStageChangedEventPayload,
|
||||
StorageValueModes,
|
||||
ChallengeObserver,
|
||||
SyncOptions,
|
||||
ImportDataReturnType,
|
||||
ImportDataUseCase,
|
||||
StoragePersistencePolicies,
|
||||
@@ -57,7 +56,6 @@ import {
|
||||
ApplicationInterface,
|
||||
EncryptionService,
|
||||
EncryptionServiceEvent,
|
||||
ChallengePrompt,
|
||||
Challenge,
|
||||
ErrorAlertStrings,
|
||||
SessionsClientInterface,
|
||||
@@ -75,32 +73,33 @@ import {
|
||||
VaultInviteServiceInterface,
|
||||
NotificationServiceEvent,
|
||||
VaultLockServiceInterface,
|
||||
ApplicationConstructorOptions,
|
||||
FullyResolvedApplicationOptions,
|
||||
ApplicationOptionsDefaults,
|
||||
ChangeAndSaveItem,
|
||||
ProtectionEvent,
|
||||
GetHost,
|
||||
SetHost,
|
||||
MfaServiceInterface,
|
||||
} from '@standardnotes/services'
|
||||
import {
|
||||
PayloadEmitSource,
|
||||
SNNote,
|
||||
PrefKey,
|
||||
PrefValue,
|
||||
DecryptedItemMutator,
|
||||
BackupFile,
|
||||
DecryptedItemInterface,
|
||||
EncryptedItemInterface,
|
||||
Environment,
|
||||
ItemStream,
|
||||
Platform,
|
||||
MutationType,
|
||||
} from '@standardnotes/models'
|
||||
import {
|
||||
HttpResponse,
|
||||
SessionListResponse,
|
||||
User,
|
||||
SignInResponse,
|
||||
ClientDisplayableError,
|
||||
SessionListEntry,
|
||||
} from '@standardnotes/responses'
|
||||
import {
|
||||
SyncService,
|
||||
ProtectionEvent,
|
||||
SettingsService,
|
||||
ActionsService,
|
||||
ChallengeResponse,
|
||||
@@ -116,14 +115,13 @@ import {
|
||||
UuidGenerator,
|
||||
useBoolean,
|
||||
LoggerInterface,
|
||||
canBlockDeinit,
|
||||
} from '@standardnotes/utils'
|
||||
import { UuidString, ApplicationEventPayload } from '../Types'
|
||||
import { applicationEventForSyncEvent } from '@Lib/Application/Event'
|
||||
import { BackupServiceInterface, FilesClientInterface } from '@standardnotes/files'
|
||||
import { ComputePrivateUsername } from '@standardnotes/encryption'
|
||||
import { SNLog } from '../Log'
|
||||
import { ApplicationConstructorOptions, FullyResolvedApplicationOptions } from './Options/ApplicationOptions'
|
||||
import { ApplicationOptionsDefaults } from './Options/Defaults'
|
||||
import { SignInWithRecoveryCodes } from '@Lib/Domain/UseCase/SignInWithRecoveryCodes/SignInWithRecoveryCodes'
|
||||
import { UseCaseContainerInterface } from '@Lib/Domain/UseCase/UseCaseContainerInterface'
|
||||
import { GetRecoveryCodes } from '@Lib/Domain/UseCase/GetRecoveryCodes/GetRecoveryCodes'
|
||||
@@ -137,7 +135,6 @@ import { GetAuthenticatorAuthenticationResponse } from '@Lib/Domain/UseCase/GetA
|
||||
import { GetAuthenticatorAuthenticationOptions } from '@Lib/Domain/UseCase/GetAuthenticatorAuthenticationOptions/GetAuthenticatorAuthenticationOptions'
|
||||
import { Dependencies } from './Dependencies/Dependencies'
|
||||
import { TYPES } from './Dependencies/Types'
|
||||
import { canBlockDeinit } from './Dependencies/isDeinitable'
|
||||
|
||||
/** How often to automatically sync, in milliseconds */
|
||||
const DEFAULT_AUTO_SYNC_INTERVAL = 30_000
|
||||
@@ -165,7 +162,6 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
|
||||
private eventHandlers: ApplicationObserver[] = []
|
||||
|
||||
private streamRemovers: ObserverRemover[] = []
|
||||
private serviceObservers: ObserverRemover[] = []
|
||||
private managedSubscribers: ObserverRemover[] = []
|
||||
private autoSyncInterval!: ReturnType<typeof setInterval>
|
||||
@@ -178,7 +174,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
private launched = false
|
||||
/** Whether the application has been destroyed via .deinit() */
|
||||
public dealloced = false
|
||||
private isBiometricsSoftLockEngaged = false
|
||||
|
||||
private revokingSession = false
|
||||
private handledFullSyncStage = false
|
||||
|
||||
@@ -561,13 +557,6 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
void this.migrations.handleApplicationEvent(event)
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the local database has completed loading local items.
|
||||
*/
|
||||
public isDatabaseLoaded(): boolean {
|
||||
return this.sync.isDatabaseLoaded()
|
||||
}
|
||||
|
||||
public getSessions(): Promise<HttpResponse<SessionListEntry[]>> {
|
||||
return this.sessions.getSessionsList()
|
||||
}
|
||||
@@ -594,65 +583,12 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
return compareVersions(userVersion, ProtocolVersion.V004) >= 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 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: string | string[],
|
||||
stream: ItemStream<I>,
|
||||
): () => void {
|
||||
const removeItemManagerObserver = this.items.addObserver<I>(
|
||||
contentType,
|
||||
({ changed, inserted, removed, source }) => {
|
||||
stream({ changed, inserted, removed, source })
|
||||
},
|
||||
)
|
||||
|
||||
const matches = this.items.getItems<I>(contentType)
|
||||
stream({
|
||||
inserted: matches,
|
||||
changed: [],
|
||||
removed: [],
|
||||
source: PayloadEmitSource.InitialObserverRegistrationPush,
|
||||
})
|
||||
|
||||
this.streamRemovers.push(removeItemManagerObserver)
|
||||
|
||||
return () => {
|
||||
removeItemManagerObserver()
|
||||
|
||||
removeFromArray(this.streamRemovers, removeItemManagerObserver)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the server's URL
|
||||
*/
|
||||
public async setHost(host: string): Promise<void> {
|
||||
this.http.setHost(host)
|
||||
|
||||
await this.legacyApi.setHost(host)
|
||||
}
|
||||
|
||||
public getHost(): string {
|
||||
return this.legacyApi.getHost()
|
||||
}
|
||||
|
||||
public async setCustomHost(host: string): Promise<void> {
|
||||
await this.setHost(host)
|
||||
await this.setHost.execute(host)
|
||||
|
||||
this.sockets.setWebSocketUrl(undefined)
|
||||
}
|
||||
|
||||
public getUser(): User | undefined {
|
||||
if (!this.launched) {
|
||||
throw Error('Attempting to access user before application unlocked')
|
||||
}
|
||||
return this.sessions.getUser()
|
||||
}
|
||||
|
||||
public getUserPasswordCreationDate(): Date | undefined {
|
||||
return this.encryption.getPasswordCreatedDate()
|
||||
}
|
||||
@@ -699,10 +635,6 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
return result
|
||||
}
|
||||
|
||||
public noAccount(): boolean {
|
||||
return !this.hasAccount()
|
||||
}
|
||||
|
||||
public hasAccount(): boolean {
|
||||
return this.encryption.hasAccount()
|
||||
}
|
||||
@@ -715,10 +647,6 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
return this.protections.hasProtectionSources()
|
||||
}
|
||||
|
||||
public hasUnprotectedAccessSession(): boolean {
|
||||
return this.protections.hasUnprotectedAccessSession()
|
||||
}
|
||||
|
||||
/**
|
||||
* When a user specifies a non-zero remember duration on a protection
|
||||
* challenge, a session will be started during which protections are disabled.
|
||||
@@ -746,10 +674,6 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
return this.protections.authorizeAutolockIntervalChange()
|
||||
}
|
||||
|
||||
public authorizeSearchingProtectedNotesText(): Promise<boolean> {
|
||||
return this.protections.authorizeSearchingProtectedNotesText()
|
||||
}
|
||||
|
||||
public async createEncryptedBackupFileForAutomatedDesktopBackups(): Promise<BackupFile | undefined> {
|
||||
return this.encryption.createEncryptedBackupFile()
|
||||
}
|
||||
@@ -852,7 +776,6 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
|
||||
this.serviceObservers.length = 0
|
||||
this.managedSubscribers.length = 0
|
||||
this.streamRemovers.length = 0
|
||||
|
||||
this.started = false
|
||||
|
||||
@@ -921,39 +844,6 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
})
|
||||
}
|
||||
|
||||
public async changeAndSaveItem<M extends DecryptedItemMutator = DecryptedItemMutator>(
|
||||
itemToLookupUuidFor: DecryptedItemInterface,
|
||||
mutate: (mutator: M) => void,
|
||||
updateTimestamps = true,
|
||||
emitSource?: PayloadEmitSource,
|
||||
syncOptions?: SyncOptions,
|
||||
): Promise<DecryptedItemInterface | undefined> {
|
||||
await this.mutator.changeItems(
|
||||
[itemToLookupUuidFor],
|
||||
mutate,
|
||||
updateTimestamps ? MutationType.UpdateUserTimestamps : MutationType.NoUpdateUserTimestamps,
|
||||
emitSource,
|
||||
)
|
||||
await this.sync.sync(syncOptions)
|
||||
return this.items.findItem(itemToLookupUuidFor.uuid)
|
||||
}
|
||||
|
||||
public async changeAndSaveItems<M extends DecryptedItemMutator = DecryptedItemMutator>(
|
||||
itemsToLookupUuidsFor: DecryptedItemInterface[],
|
||||
mutate: (mutator: M) => void,
|
||||
updateTimestamps = true,
|
||||
emitSource?: PayloadEmitSource,
|
||||
syncOptions?: SyncOptions,
|
||||
): Promise<void> {
|
||||
await this.mutator.changeItems(
|
||||
itemsToLookupUuidsFor,
|
||||
mutate,
|
||||
updateTimestamps ? MutationType.UpdateUserTimestamps : MutationType.NoUpdateUserTimestamps,
|
||||
emitSource,
|
||||
)
|
||||
await this.sync.sync(syncOptions)
|
||||
}
|
||||
|
||||
public async importData(data: BackupFile, awaitSync = false): Promise<ImportDataReturnType> {
|
||||
const usecase = this.dependencies.get<ImportDataUseCase>(TYPES.ImportDataUseCase)
|
||||
return usecase.execute(data, awaitSync)
|
||||
@@ -991,42 +881,18 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
return this.encryption.hasPasscode()
|
||||
}
|
||||
|
||||
async isLocked(): Promise<boolean> {
|
||||
if (!this.started) {
|
||||
return Promise.resolve(true)
|
||||
}
|
||||
const isPasscodeLocked = await this.challenges.isPasscodeLocked()
|
||||
return isPasscodeLocked || this.isBiometricsSoftLockEngaged
|
||||
}
|
||||
|
||||
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. */
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
|
||||
public softLockBiometrics(): void {
|
||||
const challenge = new Challenge(
|
||||
[new ChallengePrompt(ChallengeValidation.Biometric)],
|
||||
ChallengeReason.ApplicationUnlock,
|
||||
false,
|
||||
)
|
||||
|
||||
void this.challenges.promptForChallengeResponse(challenge)
|
||||
|
||||
this.isBiometricsSoftLockEngaged = true
|
||||
void this.notifyEvent(ApplicationEvent.BiometricsSoftLockEngaged)
|
||||
|
||||
this.addChallengeObserver(challenge, {
|
||||
onComplete: () => {
|
||||
this.isBiometricsSoftLockEngaged = false
|
||||
void this.notifyEvent(ApplicationEvent.BiometricsSoftLockDisengaged)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
isNativeMobileWeb() {
|
||||
return this.environment === Environment.Mobile
|
||||
}
|
||||
@@ -1102,10 +968,6 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
}
|
||||
}
|
||||
|
||||
public getNewSubscriptionToken(): Promise<string | undefined> {
|
||||
return this.legacyApi.getNewSubscriptionToken()
|
||||
}
|
||||
|
||||
public isThirdPartyHostUsed(): boolean {
|
||||
return this.legacyApi.isThirdPartyHostUsed()
|
||||
}
|
||||
@@ -1117,7 +979,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
return false
|
||||
}
|
||||
|
||||
return this.getHost() === (await homeServerService.getHomeServerUrl())
|
||||
return this.getHost.execute().getValue() === (await homeServerService.getHomeServerUrl())
|
||||
}
|
||||
|
||||
private createBackgroundDependencies() {
|
||||
@@ -1361,14 +1223,30 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
return this.dependencies.get<SharedVaultServiceInterface>(TYPES.SharedVaultService)
|
||||
}
|
||||
|
||||
private get migrations(): MigrationService {
|
||||
return this.dependencies.get<MigrationService>(TYPES.MigrationService)
|
||||
public get changeAndSaveItem(): ChangeAndSaveItem {
|
||||
return this.dependencies.get<ChangeAndSaveItem>(TYPES.ChangeAndSaveItem)
|
||||
}
|
||||
|
||||
private get legacyApi(): LegacyApiService {
|
||||
public get getHost(): GetHost {
|
||||
return this.dependencies.get<GetHost>(TYPES.GetHost)
|
||||
}
|
||||
|
||||
public get setHost(): SetHost {
|
||||
return this.dependencies.get<SetHost>(TYPES.SetHost)
|
||||
}
|
||||
|
||||
public get legacyApi(): LegacyApiService {
|
||||
return this.dependencies.get<LegacyApiService>(TYPES.LegacyApiService)
|
||||
}
|
||||
|
||||
public get mfa(): MfaServiceInterface {
|
||||
return this.dependencies.get<MfaService>(TYPES.MfaService)
|
||||
}
|
||||
|
||||
private get migrations(): MigrationService {
|
||||
return this.dependencies.get<MigrationService>(TYPES.MigrationService)
|
||||
}
|
||||
|
||||
private get http(): HttpServiceInterface {
|
||||
return this.dependencies.get<HttpServiceInterface>(TYPES.HttpService)
|
||||
}
|
||||
@@ -1376,8 +1254,4 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
private get sockets(): WebSocketsService {
|
||||
return this.dependencies.get<WebSocketsService>(TYPES.WebSocketsService)
|
||||
}
|
||||
|
||||
private get mfa(): SNMfaService {
|
||||
return this.dependencies.get<SNMfaService>(TYPES.MfaService)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { GetRecoveryCodes } from '../../Domain/UseCase/GetRecoveryCodes/GetRecov
|
||||
import { SignInWithRecoveryCodes } from '../../Domain/UseCase/SignInWithRecoveryCodes/SignInWithRecoveryCodes'
|
||||
import { ListedService } from '../../Services/Listed/ListedService'
|
||||
import { MigrationService } from '../../Services/Migration/MigrationService'
|
||||
import { SNMfaService } from '../../Services/Mfa/MfaService'
|
||||
import { MfaService } from '../../Services/Mfa/MfaService'
|
||||
import { SNComponentManager } from '../../Services/ComponentManager/ComponentManager'
|
||||
import { FeaturesService } from '@Lib/Services/Features/FeaturesService'
|
||||
import { SettingsService } from '../../Services/Settings/SNSettingsService'
|
||||
@@ -126,6 +126,10 @@ import {
|
||||
AlertService,
|
||||
DesktopDeviceInterface,
|
||||
ChangeVaultStorageMode,
|
||||
ChangeAndSaveItem,
|
||||
FullyResolvedApplicationOptions,
|
||||
GetHost,
|
||||
SetHost,
|
||||
} from '@standardnotes/services'
|
||||
import { ItemManager } from '../../Services/Items/ItemManager'
|
||||
import { PayloadManager } from '../../Services/Payloads/PayloadManager'
|
||||
@@ -151,10 +155,8 @@ import {
|
||||
WebSocketApiService,
|
||||
WebSocketServer,
|
||||
} from '@standardnotes/api'
|
||||
import { FullyResolvedApplicationOptions } from '../Options/ApplicationOptions'
|
||||
import { TYPES } from './Types'
|
||||
import { isDeinitable } from './isDeinitable'
|
||||
import { Logger, isNotUndefined } from '@standardnotes/utils'
|
||||
import { Logger, isNotUndefined, isDeinitable } from '@standardnotes/utils'
|
||||
import { EncryptionOperators } from '@standardnotes/encryption'
|
||||
import { AsymmetricMessagePayload, AsymmetricMessageSharedVaultInvite } from '@standardnotes/models'
|
||||
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||
@@ -219,6 +221,14 @@ export class Dependencies {
|
||||
)
|
||||
})
|
||||
|
||||
this.factory.set(TYPES.GetHost, () => {
|
||||
return new GetHost(this.get<LegacyApiService>(TYPES.LegacyApiService))
|
||||
})
|
||||
|
||||
this.factory.set(TYPES.SetHost, () => {
|
||||
return new SetHost(this.get<HttpService>(TYPES.HttpService), this.get<LegacyApiService>(TYPES.LegacyApiService))
|
||||
})
|
||||
|
||||
this.factory.set(TYPES.GetKeyPairs, () => {
|
||||
return new GetKeyPairs(this.get<RootKeyManager>(TYPES.RootKeyManager))
|
||||
})
|
||||
@@ -307,6 +317,14 @@ export class Dependencies {
|
||||
return new GetVaults(this.get<ItemManager>(TYPES.ItemManager))
|
||||
})
|
||||
|
||||
this.factory.set(TYPES.ChangeAndSaveItem, () => {
|
||||
return new ChangeAndSaveItem(
|
||||
this.get<ItemManager>(TYPES.ItemManager),
|
||||
this.get<MutatorService>(TYPES.MutatorService),
|
||||
this.get<SyncService>(TYPES.SyncService),
|
||||
)
|
||||
})
|
||||
|
||||
this.factory.set(TYPES.GetSharedVaults, () => {
|
||||
return new GetSharedVaults(this.get<GetVaults>(TYPES.GetVaults))
|
||||
})
|
||||
@@ -1080,7 +1098,7 @@ export class Dependencies {
|
||||
})
|
||||
|
||||
this.factory.set(TYPES.MfaService, () => {
|
||||
return new SNMfaService(
|
||||
return new MfaService(
|
||||
this.get<SettingsService>(TYPES.SettingsService),
|
||||
this.get<PureCryptoInterface>(TYPES.Crypto),
|
||||
this.get<FeaturesService>(TYPES.FeaturesService),
|
||||
|
||||
@@ -156,6 +156,9 @@ export const TYPES = {
|
||||
DecryptErroredPayloads: Symbol.for('DecryptErroredPayloads'),
|
||||
GetKeyPairs: Symbol.for('GetKeyPairs'),
|
||||
ChangeVaultStorageMode: Symbol.for('ChangeVaultStorageMode'),
|
||||
ChangeAndSaveItem: Symbol.for('ChangeAndSaveItem'),
|
||||
GetHost: Symbol.for('GetHost'),
|
||||
SetHost: Symbol.for('SetHost'),
|
||||
|
||||
// Mappers
|
||||
SessionStorageMapper: Symbol.for('SessionStorageMapper'),
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
export function isDeinitable(service: unknown): service is { deinit(): void } {
|
||||
if (!service) {
|
||||
throw new Error('Service is undefined')
|
||||
}
|
||||
return typeof (service as { deinit(): void }).deinit === 'function'
|
||||
}
|
||||
|
||||
export function canBlockDeinit(service: unknown): service is { blockDeinit(): Promise<void> } {
|
||||
if (!service) {
|
||||
throw new Error('Service is undefined')
|
||||
}
|
||||
|
||||
return typeof (service as { blockDeinit(): Promise<void> }).blockDeinit === 'function'
|
||||
}
|
||||
@@ -1,17 +1,17 @@
|
||||
import { DecryptedItemInterface } from '@standardnotes/models'
|
||||
import { ApplicationInterface } from '@standardnotes/services'
|
||||
import { ItemManagerInterface } from '@standardnotes/services'
|
||||
|
||||
/** Keeps an item reference up to date with changes */
|
||||
export class LiveItem<T extends DecryptedItemInterface> {
|
||||
public item: T
|
||||
private removeObserver: () => void
|
||||
|
||||
constructor(uuid: string, application: ApplicationInterface, onChange?: (item: T) => void) {
|
||||
this.item = application.items.findSureItem(uuid)
|
||||
constructor(uuid: string, items: ItemManagerInterface, onChange?: (item: T) => void) {
|
||||
this.item = items.findSureItem(uuid)
|
||||
|
||||
onChange && onChange(this.item)
|
||||
|
||||
this.removeObserver = application.streamItems(this.item.content_type, ({ changed, inserted }) => {
|
||||
this.removeObserver = items.streamItems(this.item.content_type, ({ changed, inserted }) => {
|
||||
const matchingItem = [...changed, ...inserted].find((item) => {
|
||||
return item.uuid === uuid
|
||||
})
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { ApplicationOptionsWhichHaveDefaults } from './Defaults'
|
||||
import {
|
||||
ApplicationDisplayOptions,
|
||||
ApplicationOptionalConfiguratioOptions,
|
||||
ApplicationSyncOptions,
|
||||
} from './OptionalOptions'
|
||||
import { RequiredApplicationOptions } from './RequiredOptions'
|
||||
|
||||
export type ApplicationConstructorOptions = RequiredApplicationOptions &
|
||||
Partial<ApplicationSyncOptions & ApplicationDisplayOptions & ApplicationOptionalConfiguratioOptions>
|
||||
|
||||
export type FullyResolvedApplicationOptions = RequiredApplicationOptions &
|
||||
ApplicationSyncOptions &
|
||||
ApplicationDisplayOptions &
|
||||
ApplicationOptionalConfiguratioOptions &
|
||||
ApplicationOptionsWhichHaveDefaults
|
||||
@@ -1,15 +0,0 @@
|
||||
import { ApplicationDisplayOptions, ApplicationSyncOptions } from './OptionalOptions'
|
||||
|
||||
export interface ApplicationOptionsWhichHaveDefaults {
|
||||
loadBatchSize: ApplicationSyncOptions['loadBatchSize']
|
||||
sleepBetweenBatches: ApplicationSyncOptions['sleepBetweenBatches']
|
||||
allowNoteSelectionStatePersistence: ApplicationDisplayOptions['allowNoteSelectionStatePersistence']
|
||||
allowMultipleSelection: ApplicationDisplayOptions['allowMultipleSelection']
|
||||
}
|
||||
|
||||
export const ApplicationOptionsDefaults: ApplicationOptionsWhichHaveDefaults = {
|
||||
loadBatchSize: 700,
|
||||
sleepBetweenBatches: 10,
|
||||
allowMultipleSelection: true,
|
||||
allowNoteSelectionStatePersistence: true,
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
export interface ApplicationSyncOptions {
|
||||
/**
|
||||
* The size of the item batch to decrypt and render upon application load.
|
||||
*/
|
||||
loadBatchSize: number
|
||||
|
||||
sleepBetweenBatches: number
|
||||
}
|
||||
|
||||
export interface ApplicationDisplayOptions {
|
||||
allowNoteSelectionStatePersistence: boolean
|
||||
allowMultipleSelection: boolean
|
||||
}
|
||||
|
||||
export interface ApplicationOptionalConfiguratioOptions {
|
||||
/**
|
||||
* URL for WebSocket providing permissions and roles information.
|
||||
*/
|
||||
webSocketUrl?: string
|
||||
|
||||
/**
|
||||
* 3rd party library function for prompting U2F authenticator device registration
|
||||
*
|
||||
* @param registrationOptions - Registration options generated by the server
|
||||
* @returns authenticator device response
|
||||
*/
|
||||
u2fAuthenticatorRegistrationPromptFunction?: (
|
||||
registrationOptions: Record<string, unknown>,
|
||||
) => Promise<Record<string, unknown>>
|
||||
|
||||
/**
|
||||
* 3rd party library function for prompting U2F authenticator device authentication
|
||||
*
|
||||
* @param registrationOptions - Registration options generated by the server
|
||||
* @returns authenticator device response
|
||||
*/
|
||||
u2fAuthenticatorVerificationPromptFunction?: (
|
||||
authenticationOptions: Record<string, unknown>,
|
||||
) => Promise<Record<string, unknown>>
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import { Environment, Platform } from '@standardnotes/models'
|
||||
import { ApplicationIdentifier } from '@standardnotes/common'
|
||||
import { AlertService, DeviceInterface } from '@standardnotes/services'
|
||||
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||
|
||||
export interface RequiredApplicationOptions {
|
||||
/**
|
||||
* The Environment that identifies your application.
|
||||
*/
|
||||
environment: Environment
|
||||
/**
|
||||
* The Platform that identifies your application.
|
||||
*/
|
||||
platform: Platform
|
||||
/**
|
||||
* The device interface that provides platform specific
|
||||
* utilities that are used to read/write raw values from/to the database or value storage.
|
||||
*/
|
||||
deviceInterface: DeviceInterface
|
||||
/**
|
||||
* The platform-dependent implementation of SNPureCrypto to use.
|
||||
* Web uses SNWebCrypto, mobile uses SNReactNativeCrypto.
|
||||
*/
|
||||
crypto: PureCryptoInterface
|
||||
/**
|
||||
* The platform-dependent implementation of alert service.
|
||||
*/
|
||||
alertService: AlertService
|
||||
/**
|
||||
* A unique persistent identifier to namespace storage and other
|
||||
* persistent properties. For an ephemeral runtime identifier, use ephemeralIdentifier.
|
||||
*/
|
||||
identifier: ApplicationIdentifier
|
||||
|
||||
/**
|
||||
* Default host to use in ApiService.
|
||||
*/
|
||||
defaultHost: string
|
||||
/**
|
||||
* Version of client application.
|
||||
*/
|
||||
appVersion: string
|
||||
}
|
||||
@@ -2,4 +2,3 @@ export * from './Application'
|
||||
export * from './Event'
|
||||
export * from './LiveItem'
|
||||
export * from './Platforms'
|
||||
export * from './Options/Defaults'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { RootKeyInterface } from '@standardnotes/models'
|
||||
import { DiskStorageService } from '../Storage/DiskStorageService'
|
||||
import { removeFromArray } from '@standardnotes/utils'
|
||||
import { isValidProtectionSessionLength } from '../Protection/ProtectionService'
|
||||
import { isValidProtectionSessionLength } from '../Protection/isValidProtectionSessionLength'
|
||||
import {
|
||||
AbstractService,
|
||||
ChallengeServiceInterface,
|
||||
@@ -158,10 +158,6 @@ export class ChallengeService extends AbstractService implements ChallengeServic
|
||||
return { wrappingKey }
|
||||
}
|
||||
|
||||
public isPasscodeLocked(): Promise<boolean> {
|
||||
return this.encryptionService.isPasscodeLocked()
|
||||
}
|
||||
|
||||
public addChallengeObserver(challenge: Challenge, observer: ChallengeObserver): () => void {
|
||||
const observers = this.challengeObservers[challenge.id] || []
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ describe('GetFeatureUrl', () => {
|
||||
})
|
||||
|
||||
describe('desktop', () => {
|
||||
let desktopManager: DesktopManagerInterface | undefined
|
||||
let desktopManager: jest.Mocked<DesktopManagerInterface | undefined>
|
||||
|
||||
beforeEach(() => {
|
||||
desktopManager = {
|
||||
@@ -69,7 +69,7 @@ describe('GetFeatureUrl', () => {
|
||||
getExtServerHost() {
|
||||
return desktopExtHost
|
||||
},
|
||||
}
|
||||
} as unknown as jest.Mocked<DesktopManagerInterface | undefined>
|
||||
|
||||
usecase = new GetFeatureUrl(desktopManager, Environment.Desktop, Platform.MacDesktop)
|
||||
})
|
||||
|
||||
@@ -28,6 +28,7 @@ export class ItemManager extends Services.AbstractService implements Services.It
|
||||
private collection!: Models.ItemCollection
|
||||
private systemSmartViews: Models.SmartView[]
|
||||
private itemCounter!: Models.ItemCounter
|
||||
private streamDisposers: (() => void)[] = []
|
||||
|
||||
private navigationDisplayController!: Models.ItemDisplayController<
|
||||
Models.SNNote | Models.FileItem,
|
||||
@@ -230,6 +231,7 @@ export class ItemManager extends Services.AbstractService implements Services.It
|
||||
|
||||
public override deinit(): void {
|
||||
this.unsubChangeObserver()
|
||||
this.streamDisposers.length = 0
|
||||
;(this.unsubChangeObserver as unknown) = undefined
|
||||
;(this.payloadManager as unknown) = undefined
|
||||
;(this.collection as unknown) = undefined
|
||||
@@ -865,4 +867,34 @@ export class ItemManager extends Services.AbstractService implements Services.It
|
||||
getNoteLinkedFiles(note: Models.SNNote): Models.FileItem[] {
|
||||
return this.itemsReferencingItem(note).filter(Models.isFile)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 Models.DecryptedItemInterface = Models.DecryptedItemInterface>(
|
||||
contentType: string | string[],
|
||||
stream: Models.ItemStream<I>,
|
||||
): () => void {
|
||||
const removeItemManagerObserver = this.addObserver<I>(contentType, ({ changed, inserted, removed, source }) => {
|
||||
stream({ changed, inserted, removed, source })
|
||||
})
|
||||
|
||||
const matches = this.getItems<I>(contentType)
|
||||
stream({
|
||||
inserted: matches,
|
||||
changed: [],
|
||||
removed: [],
|
||||
source: Models.PayloadEmitSource.InitialObserverRegistrationPush,
|
||||
})
|
||||
|
||||
this.streamDisposers.push(removeItemManagerObserver)
|
||||
|
||||
return () => {
|
||||
removeItemManagerObserver()
|
||||
|
||||
removeFromArray(this.streamDisposers, removeItemManagerObserver)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@ import { SettingName } from '@standardnotes/settings'
|
||||
import { SettingsService } from '../Settings'
|
||||
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||
import { FeaturesService } from '../Features/FeaturesService'
|
||||
import { AbstractService, InternalEventBusInterface, SignInStrings } from '@standardnotes/services'
|
||||
import { AbstractService, InternalEventBusInterface, MfaServiceInterface, SignInStrings } from '@standardnotes/services'
|
||||
|
||||
export class SNMfaService extends AbstractService {
|
||||
export class MfaService extends AbstractService implements MfaServiceInterface {
|
||||
constructor(
|
||||
private settingsService: SettingsService,
|
||||
private crypto: PureCryptoInterface,
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction = 30
|
||||
@@ -29,45 +29,11 @@ import {
|
||||
InternalEventInterface,
|
||||
ApplicationEvent,
|
||||
ApplicationStageChangedEventPayload,
|
||||
ProtectionEvent,
|
||||
} from '@standardnotes/services'
|
||||
import { ContentType } from '@standardnotes/domain-core'
|
||||
|
||||
export enum ProtectionEvent {
|
||||
UnprotectedSessionBegan = 'UnprotectedSessionBegan',
|
||||
UnprotectedSessionExpired = 'UnprotectedSessionExpired',
|
||||
}
|
||||
|
||||
export const ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction = 30
|
||||
|
||||
export enum UnprotectedAccessSecondsDuration {
|
||||
OneMinute = 60,
|
||||
FiveMinutes = 300,
|
||||
OneHour = 3600,
|
||||
OneWeek = 604800,
|
||||
}
|
||||
|
||||
export function isValidProtectionSessionLength(number: unknown): boolean {
|
||||
return typeof number === 'number' && Object.values(UnprotectedAccessSecondsDuration).includes(number)
|
||||
}
|
||||
|
||||
export const ProtectionSessionDurations = [
|
||||
{
|
||||
valueInSeconds: UnprotectedAccessSecondsDuration.OneMinute,
|
||||
label: '1 Minute',
|
||||
},
|
||||
{
|
||||
valueInSeconds: UnprotectedAccessSecondsDuration.FiveMinutes,
|
||||
label: '5 Minutes',
|
||||
},
|
||||
{
|
||||
valueInSeconds: UnprotectedAccessSecondsDuration.OneHour,
|
||||
label: '1 Hour',
|
||||
},
|
||||
{
|
||||
valueInSeconds: UnprotectedAccessSecondsDuration.OneWeek,
|
||||
label: '1 Week',
|
||||
},
|
||||
]
|
||||
import { isValidProtectionSessionLength } from './isValidProtectionSessionLength'
|
||||
import { UnprotectedAccessSecondsDuration } from './UnprotectedAccessSecondsDuration'
|
||||
|
||||
/**
|
||||
* Enforces certain actions to require extra authentication,
|
||||
@@ -82,11 +48,14 @@ export class ProtectionService
|
||||
private mobilePasscodeTiming: MobileUnlockTiming | undefined = MobileUnlockTiming.OnQuit
|
||||
private mobileBiometricsTiming: MobileUnlockTiming | undefined = MobileUnlockTiming.OnQuit
|
||||
|
||||
private isBiometricsSoftLockEngaged = false
|
||||
private applicationStarted = false
|
||||
|
||||
constructor(
|
||||
private encryptionService: EncryptionService,
|
||||
private encryption: EncryptionService,
|
||||
private mutator: MutatorClientInterface,
|
||||
private challengeService: ChallengeService,
|
||||
private storageService: DiskStorageService,
|
||||
private challenges: ChallengeService,
|
||||
private storage: DiskStorageService,
|
||||
protected override internalEventBus: InternalEventBusInterface,
|
||||
) {
|
||||
super(internalEventBus)
|
||||
@@ -94,9 +63,9 @@ export class ProtectionService
|
||||
|
||||
public override deinit(): void {
|
||||
clearTimeout(this.sessionExpiryTimeout)
|
||||
;(this.encryptionService as unknown) = undefined
|
||||
;(this.challengeService as unknown) = undefined
|
||||
;(this.storageService as unknown) = undefined
|
||||
;(this.encryption as unknown) = undefined
|
||||
;(this.challenges as unknown) = undefined
|
||||
;(this.storage as unknown) = undefined
|
||||
super.deinit()
|
||||
}
|
||||
|
||||
@@ -108,11 +77,42 @@ export class ProtectionService
|
||||
this.mobilePasscodeTiming = this.getMobilePasscodeTiming()
|
||||
this.mobileBiometricsTiming = this.getMobileBiometricsTiming()
|
||||
}
|
||||
} else if (event.type === ApplicationEvent.Started) {
|
||||
this.applicationStarted = true
|
||||
}
|
||||
}
|
||||
|
||||
async isLocked(): Promise<boolean> {
|
||||
if (!this.applicationStarted) {
|
||||
return true
|
||||
}
|
||||
|
||||
const isPasscodeLocked = await this.encryption.isPasscodeLocked()
|
||||
return isPasscodeLocked || this.isBiometricsSoftLockEngaged
|
||||
}
|
||||
|
||||
public softLockBiometrics(): void {
|
||||
const challenge = new Challenge(
|
||||
[new ChallengePrompt(ChallengeValidation.Biometric)],
|
||||
ChallengeReason.ApplicationUnlock,
|
||||
false,
|
||||
)
|
||||
|
||||
void this.challenges.promptForChallengeResponse(challenge)
|
||||
|
||||
this.isBiometricsSoftLockEngaged = true
|
||||
void this.notifyEvent(ProtectionEvent.BiometricsSoftLockEngaged)
|
||||
|
||||
this.challenges.addChallengeObserver(challenge, {
|
||||
onComplete: () => {
|
||||
this.isBiometricsSoftLockEngaged = false
|
||||
void this.notifyEvent(ProtectionEvent.BiometricsSoftLockDisengaged)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
public hasProtectionSources(): boolean {
|
||||
return this.encryptionService.hasAccount() || this.encryptionService.hasPasscode() || this.hasBiometricsEnabled()
|
||||
return this.encryption.hasAccount() || this.encryption.hasPasscode() || this.hasBiometricsEnabled()
|
||||
}
|
||||
|
||||
public hasUnprotectedAccessSession(): boolean {
|
||||
@@ -123,7 +123,7 @@ export class ProtectionService
|
||||
}
|
||||
|
||||
public hasBiometricsEnabled(): boolean {
|
||||
const biometricsState = this.storageService.getValue(StorageKey.BiometricsState, StorageValueModes.Nonwrapped)
|
||||
const biometricsState = this.storage.getValue(StorageKey.BiometricsState, StorageValueModes.Nonwrapped)
|
||||
return Boolean(biometricsState)
|
||||
}
|
||||
|
||||
@@ -133,7 +133,7 @@ export class ProtectionService
|
||||
return false
|
||||
}
|
||||
|
||||
this.storageService.setValue(StorageKey.BiometricsState, true, StorageValueModes.Nonwrapped)
|
||||
this.storage.setValue(StorageKey.BiometricsState, true, StorageValueModes.Nonwrapped)
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -145,7 +145,7 @@ export class ProtectionService
|
||||
}
|
||||
|
||||
if (await this.validateOrRenewSession(ChallengeReason.DisableBiometrics)) {
|
||||
this.storageService.setValue(StorageKey.BiometricsState, false, StorageValueModes.Nonwrapped)
|
||||
this.storage.setValue(StorageKey.BiometricsState, false, StorageValueModes.Nonwrapped)
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
@@ -157,7 +157,7 @@ export class ProtectionService
|
||||
if (this.hasBiometricsEnabled()) {
|
||||
prompts.push(new ChallengePrompt(ChallengeValidation.Biometric))
|
||||
}
|
||||
if (this.encryptionService.hasPasscode()) {
|
||||
if (this.encryption.hasPasscode()) {
|
||||
prompts.push(new ChallengePrompt(ChallengeValidation.LocalPasscode))
|
||||
}
|
||||
if (prompts.length > 0) {
|
||||
@@ -316,7 +316,7 @@ export class ProtectionService
|
||||
}
|
||||
|
||||
getMobileBiometricsTiming(): MobileUnlockTiming | undefined {
|
||||
return this.storageService.getValue<MobileUnlockTiming | undefined>(
|
||||
return this.storage.getValue<MobileUnlockTiming | undefined>(
|
||||
StorageKey.MobileBiometricsTiming,
|
||||
StorageValueModes.Nonwrapped,
|
||||
MobileUnlockTiming.OnQuit,
|
||||
@@ -324,7 +324,7 @@ export class ProtectionService
|
||||
}
|
||||
|
||||
getMobilePasscodeTiming(): MobileUnlockTiming | undefined {
|
||||
return this.storageService.getValue<MobileUnlockTiming | undefined>(
|
||||
return this.storage.getValue<MobileUnlockTiming | undefined>(
|
||||
StorageKey.MobilePasscodeTiming,
|
||||
StorageValueModes.Nonwrapped,
|
||||
MobileUnlockTiming.OnQuit,
|
||||
@@ -332,21 +332,21 @@ export class ProtectionService
|
||||
}
|
||||
|
||||
setMobileBiometricsTiming(timing: MobileUnlockTiming): void {
|
||||
this.storageService.setValue(StorageKey.MobileBiometricsTiming, timing, StorageValueModes.Nonwrapped)
|
||||
this.storage.setValue(StorageKey.MobileBiometricsTiming, timing, StorageValueModes.Nonwrapped)
|
||||
this.mobileBiometricsTiming = timing
|
||||
}
|
||||
|
||||
setMobilePasscodeTiming(timing: MobileUnlockTiming): void {
|
||||
this.storageService.setValue(StorageKey.MobilePasscodeTiming, timing, StorageValueModes.Nonwrapped)
|
||||
this.storage.setValue(StorageKey.MobilePasscodeTiming, timing, StorageValueModes.Nonwrapped)
|
||||
this.mobilePasscodeTiming = timing
|
||||
}
|
||||
|
||||
setMobileScreenshotPrivacyEnabled(isEnabled: boolean) {
|
||||
return this.storageService.setValue(StorageKey.MobileScreenshotPrivacyEnabled, isEnabled, StorageValueModes.Default)
|
||||
return this.storage.setValue(StorageKey.MobileScreenshotPrivacyEnabled, isEnabled, StorageValueModes.Default)
|
||||
}
|
||||
|
||||
getMobileScreenshotPrivacyEnabled(): boolean {
|
||||
return this.storageService.getValue(StorageKey.MobileScreenshotPrivacyEnabled, StorageValueModes.Default, false)
|
||||
return this.storage.getValue(StorageKey.MobileScreenshotPrivacyEnabled, StorageValueModes.Default, false)
|
||||
}
|
||||
|
||||
private async validateOrRenewSession(
|
||||
@@ -363,19 +363,19 @@ export class ProtectionService
|
||||
prompts.push(new ChallengePrompt(ChallengeValidation.Biometric))
|
||||
}
|
||||
|
||||
if (this.encryptionService.hasPasscode()) {
|
||||
if (this.encryption.hasPasscode()) {
|
||||
prompts.push(new ChallengePrompt(ChallengeValidation.LocalPasscode))
|
||||
}
|
||||
|
||||
if (requireAccountPassword) {
|
||||
if (!this.encryptionService.hasAccount()) {
|
||||
if (!this.encryption.hasAccount()) {
|
||||
throw Error('Requiring account password for challenge with no account')
|
||||
}
|
||||
prompts.push(new ChallengePrompt(ChallengeValidation.AccountPassword))
|
||||
}
|
||||
|
||||
if (prompts.length === 0) {
|
||||
if (fallBackToAccountPassword && this.encryptionService.hasAccount()) {
|
||||
if (fallBackToAccountPassword && this.encryption.hasAccount()) {
|
||||
prompts.push(new ChallengePrompt(ChallengeValidation.AccountPassword))
|
||||
} else {
|
||||
return true
|
||||
@@ -396,7 +396,7 @@ export class ProtectionService
|
||||
),
|
||||
)
|
||||
|
||||
const response = await this.challengeService.promptForChallengeResponse(new Challenge(prompts, reason, true))
|
||||
const response = await this.challenges.promptForChallengeResponse(new Challenge(prompts, reason, true))
|
||||
|
||||
if (response) {
|
||||
const length = response.values.find(
|
||||
@@ -414,7 +414,7 @@ export class ProtectionService
|
||||
}
|
||||
|
||||
public getSessionExpiryDate(): Date {
|
||||
const expiresAt = this.storageService.getValue<number>(StorageKey.ProtectionExpirey)
|
||||
const expiresAt = this.storage.getValue<number>(StorageKey.ProtectionExpirey)
|
||||
if (expiresAt) {
|
||||
return new Date(expiresAt)
|
||||
} else {
|
||||
@@ -428,15 +428,15 @@ export class ProtectionService
|
||||
}
|
||||
|
||||
private setSessionExpiryDate(date: Date) {
|
||||
this.storageService.setValue(StorageKey.ProtectionExpirey, date)
|
||||
this.storage.setValue(StorageKey.ProtectionExpirey, date)
|
||||
}
|
||||
|
||||
private getLastSessionLength(): UnprotectedAccessSecondsDuration | undefined {
|
||||
return this.storageService.getValue(StorageKey.ProtectionSessionLength)
|
||||
return this.storage.getValue(StorageKey.ProtectionSessionLength)
|
||||
}
|
||||
|
||||
private setSessionLength(length: UnprotectedAccessSecondsDuration): void {
|
||||
this.storageService.setValue(StorageKey.ProtectionSessionLength, length)
|
||||
this.storage.setValue(StorageKey.ProtectionSessionLength, length)
|
||||
const expiresAt = new Date()
|
||||
expiresAt.setSeconds(expiresAt.getSeconds() + length)
|
||||
this.setSessionExpiryDate(expiresAt)
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { UnprotectedAccessSecondsDuration } from './UnprotectedAccessSecondsDuration'
|
||||
|
||||
export const ProtectionSessionDurations = [
|
||||
{
|
||||
valueInSeconds: UnprotectedAccessSecondsDuration.OneMinute,
|
||||
label: '1 Minute',
|
||||
},
|
||||
{
|
||||
valueInSeconds: UnprotectedAccessSecondsDuration.FiveMinutes,
|
||||
label: '5 Minutes',
|
||||
},
|
||||
{
|
||||
valueInSeconds: UnprotectedAccessSecondsDuration.OneHour,
|
||||
label: '1 Hour',
|
||||
},
|
||||
{
|
||||
valueInSeconds: UnprotectedAccessSecondsDuration.OneWeek,
|
||||
label: '1 Week',
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,6 @@
|
||||
export enum UnprotectedAccessSecondsDuration {
|
||||
OneMinute = 60,
|
||||
FiveMinutes = 300,
|
||||
OneHour = 3600,
|
||||
OneWeek = 604800,
|
||||
}
|
||||
@@ -1 +1,5 @@
|
||||
export * from './ProtectionService'
|
||||
export * from './ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction'
|
||||
export * from './ProtectionSessionDurations'
|
||||
export * from './UnprotectedAccessSecondsDuration'
|
||||
export * from './isValidProtectionSessionLength'
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { UnprotectedAccessSecondsDuration } from './UnprotectedAccessSecondsDuration'
|
||||
|
||||
export function isValidProtectionSessionLength(number: unknown): boolean {
|
||||
return typeof number === 'number' && Object.values(UnprotectedAccessSecondsDuration).includes(number)
|
||||
}
|
||||
@@ -264,10 +264,15 @@ export class SessionManager
|
||||
}
|
||||
}
|
||||
|
||||
/** Unlike EncryptionService.hasAccount, isSignedIn can only be read once the application is unlocked */
|
||||
public isSignedIn(): boolean {
|
||||
return this.getUser() != undefined
|
||||
}
|
||||
|
||||
public isSignedOut(): boolean {
|
||||
return !this.isSignedIn()
|
||||
}
|
||||
|
||||
public isSignedIntoFirstPartyServer(): boolean {
|
||||
return this.isSignedIn() && !this.apiService.isThirdPartyHostUsed()
|
||||
}
|
||||
|
||||
@@ -83,6 +83,7 @@ import {
|
||||
SyncEventReceivedNotificationsData,
|
||||
SyncEventReceivedAsymmetricMessagesData,
|
||||
SyncOpStatus,
|
||||
ApplicationSyncOptions,
|
||||
} from '@standardnotes/services'
|
||||
import { OfflineSyncResponse } from './Offline/Response'
|
||||
import {
|
||||
@@ -92,7 +93,6 @@ import {
|
||||
SplitPayloadsByEncryptionType,
|
||||
} from '@standardnotes/encryption'
|
||||
import { CreatePayloadFromRawServerItem } from './Account/Utilities'
|
||||
import { ApplicationSyncOptions } from '@Lib/Application/Options/OptionalOptions'
|
||||
import { DecryptedServerConflictMap, TrustedServerConflictMap } from './Account/ServerConflictMap'
|
||||
import { ContentType } from '@standardnotes/domain-core'
|
||||
|
||||
|
||||
@@ -79,8 +79,8 @@ describe('application instances', () => {
|
||||
})
|
||||
await recreatedContext.launch()
|
||||
|
||||
expect(recreatedContext.application.getHost()).to.not.equal('http://nonsense.host')
|
||||
expect(recreatedContext.application.getHost()).to.equal(Factory.getDefaultHost())
|
||||
expect(recreatedContext.application.getHost.execute().getValue()).to.not.equal('http://nonsense.host')
|
||||
expect(recreatedContext.application.getHost.execute().getValue()).to.equal(Factory.getDefaultHost())
|
||||
|
||||
await recreatedContext.deinit()
|
||||
})
|
||||
|
||||
@@ -85,7 +85,7 @@ describe('auth fringe cases', () => {
|
||||
|
||||
const serverText = 'server text'
|
||||
|
||||
await context.application.changeAndSaveItem(firstVersionOfNote, (mutator) => {
|
||||
await context.application.changeAndSaveItem.execute(firstVersionOfNote, (mutator) => {
|
||||
mutator.text = serverText
|
||||
})
|
||||
|
||||
|
||||
@@ -141,7 +141,7 @@ describe('features', () => {
|
||||
expect(await application.settings.getDoesSensitiveSettingExist(setting)).to.equal(false)
|
||||
const extensionKey = UuidGenerator.GenerateUuid().split('-').join('')
|
||||
const promise = new Promise((resolve) => {
|
||||
application.streamItems(ContentType.TYPES.ExtensionRepo, ({ changed }) => {
|
||||
application.items.streamItems(ContentType.TYPES.ExtensionRepo, ({ changed }) => {
|
||||
for (const item of changed) {
|
||||
if (item.content.migratedToUserSetting) {
|
||||
resolve()
|
||||
|
||||
@@ -34,8 +34,8 @@ describe('history manager', () => {
|
||||
await Factory.safeDeinit(this.application)
|
||||
})
|
||||
|
||||
function setTextAndSync(application, item, text) {
|
||||
return application.changeAndSaveItem(
|
||||
async function setTextAndSync(application, item, text) {
|
||||
const result = await application.changeAndSaveItem.execute(
|
||||
item,
|
||||
(mutator) => {
|
||||
mutator.text = text
|
||||
@@ -44,6 +44,8 @@ describe('history manager', () => {
|
||||
undefined,
|
||||
syncOptions,
|
||||
)
|
||||
|
||||
return result.getValue()
|
||||
}
|
||||
|
||||
function deleteCharsFromString(string, amount) {
|
||||
@@ -59,7 +61,7 @@ describe('history manager', () => {
|
||||
expect(this.history.sessionHistoryForItem(item).length).to.equal(0)
|
||||
|
||||
/** Sync with different contents, should create new entry */
|
||||
await this.application.changeAndSaveItem(
|
||||
await this.application.changeAndSaveItem.execute(
|
||||
item,
|
||||
(mutator) => {
|
||||
mutator.title = Math.random()
|
||||
@@ -79,7 +81,7 @@ describe('history manager', () => {
|
||||
const context = await Factory.createAppContext({ identifier })
|
||||
await context.launch()
|
||||
expect(context.history.sessionHistoryForItem(item).length).to.equal(0)
|
||||
await context.application.changeAndSaveItem(
|
||||
await context.application.changeAndSaveItem.execute(
|
||||
item,
|
||||
(mutator) => {
|
||||
mutator.title = Math.random()
|
||||
@@ -103,7 +105,7 @@ describe('history manager', () => {
|
||||
await context.application.mutator.insertItem(item)
|
||||
expect(context.history.sessionHistoryForItem(item).length).to.equal(0)
|
||||
|
||||
await context.application.changeAndSaveItem(
|
||||
await context.application.changeAndSaveItem.execute(
|
||||
item,
|
||||
(mutator) => {
|
||||
mutator.title = Math.random()
|
||||
@@ -243,7 +245,7 @@ describe('history manager', () => {
|
||||
const payload = Factory.createNotePayload()
|
||||
await this.application.mutator.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
|
||||
const item = this.application.items.findItem(payload.uuid)
|
||||
await this.application.changeAndSaveItem(
|
||||
await this.application.changeAndSaveItem.execute(
|
||||
item,
|
||||
(mutator) => {
|
||||
mutator.title = Math.random()
|
||||
@@ -306,7 +308,7 @@ describe('history manager', () => {
|
||||
expect(itemHistory.length).to.equal(1)
|
||||
|
||||
/** Sync with different contents, should not create a new entry */
|
||||
await this.application.changeAndSaveItem(
|
||||
await this.application.changeAndSaveItem.execute(
|
||||
item,
|
||||
(mutator) => {
|
||||
mutator.title = Math.random()
|
||||
@@ -327,7 +329,7 @@ describe('history manager', () => {
|
||||
await Factory.sleep(Factory.ServerRevisionFrequency)
|
||||
/** Sync with different contents, should create new entry */
|
||||
const newTitleAfterFirstChange = `The title should be: ${Math.random()}`
|
||||
await this.application.changeAndSaveItem(
|
||||
await this.application.changeAndSaveItem.execute(
|
||||
item,
|
||||
(mutator) => {
|
||||
mutator.title = newTitleAfterFirstChange
|
||||
@@ -411,7 +413,7 @@ describe('history manager', () => {
|
||||
await Factory.sleep(Factory.ServerRevisionFrequency)
|
||||
|
||||
const changedText = `${Math.random()}`
|
||||
await this.application.changeAndSaveItem(note, (mutator) => {
|
||||
await this.application.changeAndSaveItem.execute(note, (mutator) => {
|
||||
mutator.title = changedText
|
||||
})
|
||||
await Factory.markDirtyAndSyncItem(this.application, note)
|
||||
|
||||
@@ -813,7 +813,7 @@ describe('importing', function () {
|
||||
},
|
||||
})
|
||||
await application.launch(false)
|
||||
await application.setHost(Factory.getDefaultHost())
|
||||
await application.setHost.execute(Factory.getDefaultHost())
|
||||
|
||||
const backupFile = {
|
||||
items: [
|
||||
|
||||
@@ -50,17 +50,19 @@ describe('items', () => {
|
||||
const item = this.application.items.items[0]
|
||||
expect(item.pinned).to.not.be.ok
|
||||
|
||||
const refreshedItem = await this.application.changeAndSaveItem(
|
||||
item,
|
||||
(mutator) => {
|
||||
mutator.pinned = true
|
||||
mutator.archived = true
|
||||
mutator.locked = true
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
syncOptions,
|
||||
)
|
||||
const refreshedItem = (
|
||||
await this.application.changeAndSaveItem.execute(
|
||||
item,
|
||||
(mutator) => {
|
||||
mutator.pinned = true
|
||||
mutator.archived = true
|
||||
mutator.locked = true
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
syncOptions,
|
||||
)
|
||||
).getValue()
|
||||
expect(refreshedItem.pinned).to.equal(true)
|
||||
expect(refreshedItem.archived).to.equal(true)
|
||||
expect(refreshedItem.locked).to.equal(true)
|
||||
@@ -77,94 +79,110 @@ describe('items', () => {
|
||||
expect(item1.isItemContentEqualWith(item2)).to.equal(true)
|
||||
|
||||
// items should ignore this field when checking for equality
|
||||
item1 = await this.application.changeAndSaveItem(
|
||||
item1,
|
||||
(mutator) => {
|
||||
mutator.userModifiedDate = new Date()
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
syncOptions,
|
||||
)
|
||||
item2 = await this.application.changeAndSaveItem(
|
||||
item2,
|
||||
(mutator) => {
|
||||
mutator.userModifiedDate = undefined
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
syncOptions,
|
||||
)
|
||||
item1 = (
|
||||
await this.application.changeAndSaveItem.execute(
|
||||
item1,
|
||||
(mutator) => {
|
||||
mutator.userModifiedDate = new Date()
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
syncOptions,
|
||||
)
|
||||
).getValue()
|
||||
item2 = (
|
||||
await this.application.changeAndSaveItem.execute(
|
||||
item2,
|
||||
(mutator) => {
|
||||
mutator.userModifiedDate = undefined
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
syncOptions,
|
||||
)
|
||||
).getValue()
|
||||
|
||||
expect(item1.isItemContentEqualWith(item2)).to.equal(true)
|
||||
|
||||
item1 = await this.application.changeAndSaveItem(
|
||||
item1,
|
||||
(mutator) => {
|
||||
mutator.mutableContent.foo = 'bar'
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
syncOptions,
|
||||
)
|
||||
item1 = (
|
||||
await this.application.changeAndSaveItem.execute(
|
||||
item1,
|
||||
(mutator) => {
|
||||
mutator.mutableContent.foo = 'bar'
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
syncOptions,
|
||||
)
|
||||
).getValue()
|
||||
|
||||
expect(item1.isItemContentEqualWith(item2)).to.equal(false)
|
||||
|
||||
item2 = await this.application.changeAndSaveItem(
|
||||
item2,
|
||||
(mutator) => {
|
||||
mutator.mutableContent.foo = 'bar'
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
syncOptions,
|
||||
)
|
||||
item2 = (
|
||||
await this.application.changeAndSaveItem.execute(
|
||||
item2,
|
||||
(mutator) => {
|
||||
mutator.mutableContent.foo = 'bar'
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
syncOptions,
|
||||
)
|
||||
).getValue()
|
||||
|
||||
expect(item1.isItemContentEqualWith(item2)).to.equal(true)
|
||||
expect(item2.isItemContentEqualWith(item1)).to.equal(true)
|
||||
|
||||
item1 = await this.application.changeAndSaveItem(
|
||||
item1,
|
||||
(mutator) => {
|
||||
mutator.e2ePendingRefactor_addItemAsRelationship(item2)
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
syncOptions,
|
||||
)
|
||||
item2 = await this.application.changeAndSaveItem(
|
||||
item2,
|
||||
(mutator) => {
|
||||
mutator.e2ePendingRefactor_addItemAsRelationship(item1)
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
syncOptions,
|
||||
)
|
||||
item1 = (
|
||||
await this.application.changeAndSaveItem.execute(
|
||||
item1,
|
||||
(mutator) => {
|
||||
mutator.e2ePendingRefactor_addItemAsRelationship(item2)
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
syncOptions,
|
||||
)
|
||||
).getValue()
|
||||
item2 = (
|
||||
await this.application.changeAndSaveItem.execute(
|
||||
item2,
|
||||
(mutator) => {
|
||||
mutator.e2ePendingRefactor_addItemAsRelationship(item1)
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
syncOptions,
|
||||
)
|
||||
).getValue()
|
||||
|
||||
expect(item1.content.references.length).to.equal(1)
|
||||
expect(item2.content.references.length).to.equal(1)
|
||||
|
||||
expect(item1.isItemContentEqualWith(item2)).to.equal(false)
|
||||
|
||||
item1 = await this.application.changeAndSaveItem(
|
||||
item1,
|
||||
(mutator) => {
|
||||
mutator.removeItemAsRelationship(item2)
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
syncOptions,
|
||||
)
|
||||
item2 = await this.application.changeAndSaveItem(
|
||||
item2,
|
||||
(mutator) => {
|
||||
mutator.removeItemAsRelationship(item1)
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
syncOptions,
|
||||
)
|
||||
item1 = (
|
||||
await this.application.changeAndSaveItem.execute(
|
||||
item1,
|
||||
(mutator) => {
|
||||
mutator.removeItemAsRelationship(item2)
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
syncOptions,
|
||||
)
|
||||
).getValue()
|
||||
item2 = (
|
||||
await this.application.changeAndSaveItem.execute(
|
||||
item2,
|
||||
(mutator) => {
|
||||
mutator.removeItemAsRelationship(item1)
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
syncOptions,
|
||||
)
|
||||
).getValue()
|
||||
|
||||
expect(item1.isItemContentEqualWith(item2)).to.equal(true)
|
||||
expect(item1.content.references.length).to.equal(0)
|
||||
@@ -179,15 +197,17 @@ describe('items', () => {
|
||||
let item1 = this.application.items.getDisplayableNotes()[0]
|
||||
const item2 = this.application.items.getDisplayableNotes()[1]
|
||||
|
||||
item1 = await this.application.changeAndSaveItem(
|
||||
item1,
|
||||
(mutator) => {
|
||||
mutator.mutableContent.foo = 'bar'
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
syncOptions,
|
||||
)
|
||||
item1 = (
|
||||
await this.application.changeAndSaveItem.execute(
|
||||
item1,
|
||||
(mutator) => {
|
||||
mutator.mutableContent.foo = 'bar'
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
syncOptions,
|
||||
)
|
||||
).getValue()
|
||||
|
||||
expect(item1.content.foo).to.equal('bar')
|
||||
|
||||
|
||||
@@ -184,15 +184,17 @@ describe('notes and tags', () => {
|
||||
expect(note.content.references.length).to.equal(0)
|
||||
expect(tag.content.references.length).to.equal(1)
|
||||
|
||||
tag = await this.application.changeAndSaveItem(
|
||||
tag,
|
||||
(mutator) => {
|
||||
mutator.removeItemAsRelationship(note)
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
syncOptions,
|
||||
)
|
||||
tag = (
|
||||
await this.application.changeAndSaveItem.execute(
|
||||
tag,
|
||||
(mutator) => {
|
||||
mutator.removeItemAsRelationship(note)
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
syncOptions,
|
||||
)
|
||||
).getValue()
|
||||
|
||||
expect(this.application.items.itemsReferencingItem(note).length).to.equal(0)
|
||||
expect(tag.noteCount).to.equal(0)
|
||||
@@ -265,15 +267,17 @@ describe('notes and tags', () => {
|
||||
const notePayload = Factory.createNotePayload()
|
||||
await this.application.mutator.emitItemsFromPayloads([notePayload], PayloadEmitSource.LocalChanged)
|
||||
let note = this.application.items.getItems([ContentType.TYPES.Note])[0]
|
||||
note = await this.application.changeAndSaveItem(
|
||||
note,
|
||||
(mutator) => {
|
||||
mutator.mutableContent.title = Math.random()
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
syncOptions,
|
||||
)
|
||||
note = (
|
||||
await this.application.changeAndSaveItem.execute(
|
||||
note,
|
||||
(mutator) => {
|
||||
mutator.mutableContent.title = Math.random()
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
syncOptions,
|
||||
)
|
||||
).getValue()
|
||||
expect(note.content.title).to.not.equal(notePayload.content.title)
|
||||
})
|
||||
|
||||
|
||||
@@ -382,19 +382,19 @@ describe('protections', function () {
|
||||
this.foo = 'tar'
|
||||
application = await Factory.createInitAppWithFakeCrypto()
|
||||
await application.addPasscode('passcode')
|
||||
expect(application.hasUnprotectedAccessSession()).to.be.false
|
||||
expect(application.protections.hasUnprotectedAccessSession()).to.be.false
|
||||
})
|
||||
|
||||
it('should return true when session length has been set', async function () {
|
||||
application = await Factory.createInitAppWithFakeCrypto()
|
||||
await application.addPasscode('passcode')
|
||||
await application.protections.setSessionLength(UnprotectedAccessSecondsDuration.OneMinute)
|
||||
expect(application.hasUnprotectedAccessSession()).to.be.true
|
||||
expect(application.protections.hasUnprotectedAccessSession()).to.be.true
|
||||
})
|
||||
|
||||
it('should return true when there are no protection sources', async function () {
|
||||
application = await Factory.createInitAppWithFakeCrypto()
|
||||
expect(application.hasUnprotectedAccessSession()).to.be.true
|
||||
expect(application.protections.hasUnprotectedAccessSession()).to.be.true
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -298,7 +298,7 @@ describe('online conflict handling', function () {
|
||||
await this.application.mutator.setItemDirty(note)
|
||||
this.expectedItemCount++
|
||||
|
||||
await this.application.changeAndSaveItem(
|
||||
await this.application.changeAndSaveItem.execute(
|
||||
note,
|
||||
(mutator) => {
|
||||
// client A
|
||||
@@ -332,7 +332,7 @@ describe('online conflict handling', function () {
|
||||
await this.application.mutator.setItemDirty(note)
|
||||
this.expectedItemCount++
|
||||
|
||||
await this.application.changeAndSaveItem(
|
||||
await this.application.changeAndSaveItem.execute(
|
||||
note,
|
||||
(mutator) => {
|
||||
// client A
|
||||
@@ -602,15 +602,17 @@ describe('online conflict handling', function () {
|
||||
*/
|
||||
let tag = await Factory.createMappedTag(this.application)
|
||||
let note = await Factory.createMappedNote(this.application)
|
||||
tag = await this.application.changeAndSaveItem(
|
||||
tag,
|
||||
(mutator) => {
|
||||
mutator.e2ePendingRefactor_addItemAsRelationship(note)
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
syncOptions,
|
||||
)
|
||||
tag = (
|
||||
await this.application.changeAndSaveItem.execute(
|
||||
tag,
|
||||
(mutator) => {
|
||||
mutator.e2ePendingRefactor_addItemAsRelationship(note)
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
syncOptions,
|
||||
)
|
||||
).getValue()
|
||||
await this.application.mutator.setItemDirty(note)
|
||||
this.expectedItemCount += 2
|
||||
|
||||
@@ -732,39 +734,42 @@ describe('online conflict handling', function () {
|
||||
})
|
||||
|
||||
/** This test takes too long on Docker CI */
|
||||
it.skip('registering for account with bulk offline data belonging to another account should be error-free', async function () {
|
||||
/**
|
||||
* When performing a multi-page sync request where we are uploading data imported from a backup,
|
||||
* if the first page of the sync request returns conflicted items keys, we rotate their UUID.
|
||||
* The second page of sync waiting to be sent up is still encrypted with the old items key UUID.
|
||||
* This causes a problem because when that second page is returned as conflicts, we will be looking
|
||||
* for an items_key_id that no longer exists (has been rotated). Rather than modifying the entire
|
||||
* sync paradigm to allow multi-page requests to consider side-effects of each page, we will instead
|
||||
* take the approach of making sure the decryption function is liberal with regards to searching
|
||||
* for the right items key. It will now consider (as a result of this test) an items key as being
|
||||
* the correct key to decrypt an item if the itemskey.uuid == item.items_key_id OR if the itemsKey.duplicateOf
|
||||
* value is equal to item.items_key_id.
|
||||
*/
|
||||
it.skip(
|
||||
'registering for account with bulk offline data belonging to another account should be error-free',
|
||||
async function () {
|
||||
/**
|
||||
* When performing a multi-page sync request where we are uploading data imported from a backup,
|
||||
* if the first page of the sync request returns conflicted items keys, we rotate their UUID.
|
||||
* The second page of sync waiting to be sent up is still encrypted with the old items key UUID.
|
||||
* This causes a problem because when that second page is returned as conflicts, we will be looking
|
||||
* for an items_key_id that no longer exists (has been rotated). Rather than modifying the entire
|
||||
* sync paradigm to allow multi-page requests to consider side-effects of each page, we will instead
|
||||
* take the approach of making sure the decryption function is liberal with regards to searching
|
||||
* for the right items key. It will now consider (as a result of this test) an items key as being
|
||||
* the correct key to decrypt an item if the itemskey.uuid == item.items_key_id OR if the itemsKey.duplicateOf
|
||||
* value is equal to item.items_key_id.
|
||||
*/
|
||||
|
||||
/** Create bulk data belonging to another account and sync */
|
||||
const largeItemCount = SyncUpDownLimit + 10
|
||||
await Factory.createManyMappedNotes(this.application, largeItemCount)
|
||||
await this.application.sync.sync(syncOptions)
|
||||
const priorData = this.application.items.items
|
||||
/** Create bulk data belonging to another account and sync */
|
||||
const largeItemCount = SyncUpDownLimit + 10
|
||||
await Factory.createManyMappedNotes(this.application, largeItemCount)
|
||||
await this.application.sync.sync(syncOptions)
|
||||
const priorData = this.application.items.items
|
||||
|
||||
/** Register new account and import this same data */
|
||||
const newApp = await Factory.signOutApplicationAndReturnNew(this.application)
|
||||
await Factory.registerUserToApplication({
|
||||
application: newApp,
|
||||
email: Utils.generateUuid(),
|
||||
password: Utils.generateUuid(),
|
||||
})
|
||||
await newApp.mutator.emitItemsFromPayloads(priorData.map((i) => i.payload))
|
||||
await newApp.sync.markAllItemsAsNeedingSyncAndPersist()
|
||||
await newApp.sync.sync(syncOptions)
|
||||
expect(newApp.payloads.invalidPayloads.length).to.equal(0)
|
||||
await Factory.safeDeinit(newApp)
|
||||
}).timeout(80000)
|
||||
/** Register new account and import this same data */
|
||||
const newApp = await Factory.signOutApplicationAndReturnNew(this.application)
|
||||
await Factory.registerUserToApplication({
|
||||
application: newApp,
|
||||
email: Utils.generateUuid(),
|
||||
password: Utils.generateUuid(),
|
||||
})
|
||||
await newApp.mutator.emitItemsFromPayloads(priorData.map((i) => i.payload))
|
||||
await newApp.sync.markAllItemsAsNeedingSyncAndPersist()
|
||||
await newApp.sync.sync(syncOptions)
|
||||
expect(newApp.payloads.invalidPayloads.length).to.equal(0)
|
||||
await Factory.safeDeinit(newApp)
|
||||
},
|
||||
).timeout(80000)
|
||||
|
||||
it('importing data belonging to another account should not result in duplication', async function () {
|
||||
/** Create primary account and export data */
|
||||
@@ -801,7 +806,7 @@ describe('online conflict handling', function () {
|
||||
await createSyncedNoteWithTag(this.application)
|
||||
const tag = this.application.items.getDisplayableTags()[0]
|
||||
const note2 = await Factory.createMappedNote(this.application)
|
||||
await this.application.changeAndSaveItem(tag, (mutator) => {
|
||||
await this.application.changeAndSaveItem.execute(tag, (mutator) => {
|
||||
mutator.e2ePendingRefactor_addItemAsRelationship(note2)
|
||||
})
|
||||
let backupFile = await this.application.createEncryptedBackupFileForAutomatedDesktopBackups()
|
||||
|
||||
@@ -98,7 +98,7 @@ describe('offline syncing', () => {
|
||||
this.expectedItemCount++
|
||||
await this.application.sync.sync(syncOptions)
|
||||
this.application = await Factory.signOutApplicationAndReturnNew(this.application)
|
||||
expect(this.application.noAccount()).to.equal(true)
|
||||
expect(this.application.getUser()).to.not.be.ok
|
||||
expect(this.application.sessions.isSignedIn()).to.equal(false)
|
||||
expect(this.application.sessions.getUser()).to.not.be.ok
|
||||
})
|
||||
})
|
||||
|
||||
@@ -600,7 +600,7 @@ describe('online syncing', function () {
|
||||
it('saving an item after sync should persist it with content property', async function () {
|
||||
const note = await Factory.createMappedNote(this.application)
|
||||
const text = Factory.randomString(10000)
|
||||
await this.application.changeAndSaveItem(
|
||||
await this.application.changeAndSaveItem.execute(
|
||||
note,
|
||||
(mutator) => {
|
||||
mutator.text = text
|
||||
@@ -1015,7 +1015,7 @@ describe('online syncing', function () {
|
||||
it('deleting an item permanently should include it in PayloadEmitSource.PreSyncSave item change observer', async function () {
|
||||
let conditionMet = false
|
||||
|
||||
this.application.streamItems([ContentType.TYPES.Note], async ({ removed, source }) => {
|
||||
this.application.items.streamItems([ContentType.TYPES.Note], async ({ removed, source }) => {
|
||||
if (source === PayloadEmitSource.PreSyncSave && removed.length === 1) {
|
||||
conditionMet = true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user