refactor(web): dependency management (#2386)

This commit is contained in:
Mo
2023-08-05 12:48:39 -05:00
committed by GitHub
parent b07da5b663
commit d8d4052a52
274 changed files with 4065 additions and 3873 deletions

View File

@@ -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)
}
}

View File

@@ -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),

View File

@@ -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'),

View File

@@ -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'
}

View File

@@ -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
})

View File

@@ -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

View File

@@ -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,
}

View File

@@ -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>>
}

View File

@@ -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
}

View File

@@ -2,4 +2,3 @@ export * from './Application'
export * from './Event'
export * from './LiveItem'
export * from './Platforms'
export * from './Options/Defaults'

View File

@@ -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] || []

View File

@@ -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)
})

View File

@@ -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)
}
}
}

View File

@@ -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,

View File

@@ -0,0 +1 @@
export const ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction = 30

View File

@@ -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)

View File

@@ -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',
},
]

View File

@@ -0,0 +1,6 @@
export enum UnprotectedAccessSecondsDuration {
OneMinute = 60,
FiveMinutes = 300,
OneHour = 3600,
OneWeek = 604800,
}

View File

@@ -1 +1,5 @@
export * from './ProtectionService'
export * from './ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction'
export * from './ProtectionSessionDurations'
export * from './UnprotectedAccessSecondsDuration'
export * from './isValidProtectionSessionLength'

View File

@@ -0,0 +1,5 @@
import { UnprotectedAccessSecondsDuration } from './UnprotectedAccessSecondsDuration'
export function isValidProtectionSessionLength(number: unknown): boolean {
return typeof number === 'number' && Object.values(UnprotectedAccessSecondsDuration).includes(number)
}

View File

@@ -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()
}

View File

@@ -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'