From 5e8feb22d01b313843c162f07c7ab9ac9390677a Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Fri, 29 Sep 2023 21:14:24 -0400 Subject: [PATCH] Rebase: Implement FolderService --- .../browser/src/background/main.background.ts | 13 +- .../src/services/jslib-services.module.ts | 13 +- .../active-user-state-provider.service.ts | 3 - .../active-user-state.provider.ts | 6 + .../global-state-provider.service.ts | 3 - .../abstractions/global-state.provider.ts | 6 + .../platform/interfaces/active-user-state.ts | 11 + .../interfaces/{state.ts => global-state.ts} | 2 +- libs/common/src/platform/misc/key-builders.ts | 16 ++ ...ault-active-user-state-provider.service.ts | 125 ----------- ...efault-active-user-state.provider.spec.ts} | 38 ++-- .../default-active-user-state.provider.ts | 199 ++++++++++++++++++ .../default-global-state-provider.service.ts | 81 ------- .../services/default-global-state.provider.ts | 84 ++++++++ .../types/derived-state-definition.ts | 18 ++ .../src/platform/types/key-definition.ts | 52 +++++ .../src/platform/types/state-definition.ts | 19 ++ .../platform/types/state-definitions.spec.ts | 22 ++ .../src/platform/types/state-definitions.ts | 3 + libs/common/src/state-migrations/migrate.ts | 6 +- .../migrations/9-move-folder-to-owned.ts | 52 +++++ .../src/state-migrations/owned-migrator.ts | 59 ------ .../vault/services/folder/folder.service.ts | 169 ++++++--------- .../common/src/vault/types/key-definitions.ts | 6 + 24 files changed, 607 insertions(+), 399 deletions(-) delete mode 100644 libs/common/src/platform/abstractions/active-user-state-provider.service.ts create mode 100644 libs/common/src/platform/abstractions/active-user-state.provider.ts delete mode 100644 libs/common/src/platform/abstractions/global-state-provider.service.ts create mode 100644 libs/common/src/platform/abstractions/global-state.provider.ts create mode 100644 libs/common/src/platform/interfaces/active-user-state.ts rename libs/common/src/platform/interfaces/{state.ts => global-state.ts} (78%) create mode 100644 libs/common/src/platform/misc/key-builders.ts delete mode 100644 libs/common/src/platform/services/default-active-user-state-provider.service.ts rename libs/common/src/platform/services/{state-provider.service.spec.ts => default-active-user-state.provider.spec.ts} (65%) create mode 100644 libs/common/src/platform/services/default-active-user-state.provider.ts delete mode 100644 libs/common/src/platform/services/default-global-state-provider.service.ts create mode 100644 libs/common/src/platform/services/default-global-state.provider.ts create mode 100644 libs/common/src/platform/types/derived-state-definition.ts create mode 100644 libs/common/src/platform/types/key-definition.ts create mode 100644 libs/common/src/platform/types/state-definition.ts create mode 100644 libs/common/src/platform/types/state-definitions.spec.ts create mode 100644 libs/common/src/platform/types/state-definitions.ts create mode 100644 libs/common/src/state-migrations/migrations/9-move-folder-to-owned.ts delete mode 100644 libs/common/src/state-migrations/owned-migrator.ts create mode 100644 libs/common/src/vault/types/key-definitions.ts diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 3aba0c77679..4a28b0aef35 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -56,6 +56,7 @@ import { ConsoleLogService } from "@bitwarden/common/platform/services/console-l import { ContainerService } from "@bitwarden/common/platform/services/container.service"; import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation"; import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation"; +import { DefaultActiveUserStateProviderService } from "@bitwarden/common/platform/services/default-active-user-state-provider.service"; import { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.service"; import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service"; import { SystemService } from "@bitwarden/common/platform/services/system.service"; @@ -332,11 +333,21 @@ export default class MainBackground { this.encryptService, this.cipherFileUploadService ); + + // TODO: This is just to make it compile + const activeUserStateProviderService = new DefaultActiveUserStateProviderService( + this.stateService, + this.memoryStorageService, + this.storageService, + this.secureStorageService + ); + this.folderService = new BrowserFolderService( this.cryptoService, this.i18nService, this.cipherService, - this.stateService + this.stateService, + activeUserStateProviderService ); this.folderApiService = new FolderApiService(this.folderService, this.apiService); this.collectionService = new CollectionService( diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 413f384fade..5bd2946f915 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -64,6 +64,7 @@ import { TokenService } from "@bitwarden/common/auth/services/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/services/two-factor.service"; import { UserVerificationApiService } from "@bitwarden/common/auth/services/user-verification/user-verification-api.service"; import { UserVerificationService } from "@bitwarden/common/auth/services/user-verification/user-verification.service"; +import { ActiveUserStateProvider } from "@bitwarden/common/platform/abstractions/active-user-state.provider"; import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service"; import { BroadcasterService as BroadcasterServiceAbstraction } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction"; @@ -91,9 +92,9 @@ import { ConsoleLogService } from "@bitwarden/common/platform/services/console-l import { CryptoService } from "@bitwarden/common/platform/services/crypto.service"; import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation"; import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation"; +import { DefaultActiveUserStateProvider } from "@bitwarden/common/platform/services/default-active-user-state.provider"; import { EnvironmentService } from "@bitwarden/common/platform/services/environment.service"; import { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.service"; -import { ActiveUserStateProviderService, BaseActiveUserStateProviderService } from "@bitwarden/common/platform/services/state-provider.service"; import { StateService } from "@bitwarden/common/platform/services/state.service"; import { ValidationService } from "@bitwarden/common/platform/services/validation.service"; import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service"; @@ -303,6 +304,7 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction"; CryptoServiceAbstraction, I18nServiceAbstraction, CipherServiceAbstraction, + ActiveUserStateProvider, StateServiceAbstraction, ], }, @@ -720,12 +722,15 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction"; deps: [CryptoServiceAbstraction], }, { - provide: ActiveUserStateProviderService, - useClass: BaseActiveUserStateProviderService, + provide: ActiveUserStateProvider, + useClass: DefaultActiveUserStateProvider, deps: [ // TODO: Do other storage services StateServiceAbstraction, - AbstractStorageService + EncryptService, + MEMORY_STORAGE, + AbstractStorageService, + SECURE_STORAGE ] } ], diff --git a/libs/common/src/platform/abstractions/active-user-state-provider.service.ts b/libs/common/src/platform/abstractions/active-user-state-provider.service.ts deleted file mode 100644 index 538812c086e..00000000000 --- a/libs/common/src/platform/abstractions/active-user-state-provider.service.ts +++ /dev/null @@ -1,3 +0,0 @@ -export abstract class ActiveUserStateProviderService { - create: (location: StorageLocation, domainToken: DomainToken) => State; -} diff --git a/libs/common/src/platform/abstractions/active-user-state.provider.ts b/libs/common/src/platform/abstractions/active-user-state.provider.ts new file mode 100644 index 00000000000..97fa46d26ab --- /dev/null +++ b/libs/common/src/platform/abstractions/active-user-state.provider.ts @@ -0,0 +1,6 @@ +import { ActiveUserState } from "../interfaces/active-user-state"; +import { KeyDefinition } from "../types/key-definition"; + +export abstract class ActiveUserStateProvider { + create: (keyDefinition: KeyDefinition) => ActiveUserState; +} diff --git a/libs/common/src/platform/abstractions/global-state-provider.service.ts b/libs/common/src/platform/abstractions/global-state-provider.service.ts deleted file mode 100644 index c82fbedea17..00000000000 --- a/libs/common/src/platform/abstractions/global-state-provider.service.ts +++ /dev/null @@ -1,3 +0,0 @@ -export abstract class GlobalStateProviderService { - create: (location: StorageLocation, domainToken: DomainToken) => State; -} diff --git a/libs/common/src/platform/abstractions/global-state.provider.ts b/libs/common/src/platform/abstractions/global-state.provider.ts new file mode 100644 index 00000000000..cb2f9195e11 --- /dev/null +++ b/libs/common/src/platform/abstractions/global-state.provider.ts @@ -0,0 +1,6 @@ +import { GlobalState } from "../interfaces/global-state"; +import { KeyDefinition } from "../types/key-definition"; + +export abstract class GlobalStateProvider { + create: (keyDefinition: KeyDefinition) => GlobalState; +} diff --git a/libs/common/src/platform/interfaces/active-user-state.ts b/libs/common/src/platform/interfaces/active-user-state.ts new file mode 100644 index 00000000000..b2520705953 --- /dev/null +++ b/libs/common/src/platform/interfaces/active-user-state.ts @@ -0,0 +1,11 @@ +import { Observable } from "rxjs"; + +import { DerivedActiveUserState } from "../services/default-active-user-state.provider"; +import { DerivedStateDefinition } from "../types/derived-state-definition"; + +export interface ActiveUserState { + readonly state$: Observable + readonly getFromState: () => Promise + readonly update: (configureState: (state: T) => void) => Promise + createDerived: (derivedStateDefinition: DerivedStateDefinition) => DerivedActiveUserState +} diff --git a/libs/common/src/platform/interfaces/state.ts b/libs/common/src/platform/interfaces/global-state.ts similarity index 78% rename from libs/common/src/platform/interfaces/state.ts rename to libs/common/src/platform/interfaces/global-state.ts index 28aab8f444b..17b9ae19705 100644 --- a/libs/common/src/platform/interfaces/state.ts +++ b/libs/common/src/platform/interfaces/global-state.ts @@ -1,6 +1,6 @@ import { Observable } from "rxjs" -export interface State { +export interface GlobalState { update: (configureState: (state: T) => void) => Promise state$: Observable } diff --git a/libs/common/src/platform/misc/key-builders.ts b/libs/common/src/platform/misc/key-builders.ts new file mode 100644 index 00000000000..45e961f6e89 --- /dev/null +++ b/libs/common/src/platform/misc/key-builders.ts @@ -0,0 +1,16 @@ +import { KeyDefinition } from "../types/key-definition"; + +// TODO: Use Matt's `UserId` type +export function userKeyBuilder( + userId: string, + keyDefinition: KeyDefinition +): string { + return `${keyDefinition.stateDefinition.name}_${userId}_${keyDefinition.key}`; +} + +export function globalKeyBuilder( + keyDefinition: KeyDefinition +): string { + // TODO: Do we want the _global_ part? + return `${keyDefinition.stateDefinition.name}_global_${keyDefinition.key}`; +} diff --git a/libs/common/src/platform/services/default-active-user-state-provider.service.ts b/libs/common/src/platform/services/default-active-user-state-provider.service.ts deleted file mode 100644 index c81401d2bd0..00000000000 --- a/libs/common/src/platform/services/default-active-user-state-provider.service.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { BehaviorSubject, Observable, defer, distinctUntilChanged, filter, firstValueFrom, map, share, switchMap, tap } from "rxjs"; -import { Jsonify } from "type-fest"; - -import { ActiveUserStateProviderService } from "../abstractions/active-user-state-provider.service"; -import { StateService } from "../abstractions/state.service"; -import { AbstractMemoryStorageService, AbstractStorageService } from "../abstractions/storage.service"; -import { State } from "../interfaces/state"; - -import { DomainToken, StorageLocation } from "./default-global-state-provider.service"; - -class DefaultActiveUserState implements State { - private formattedKey$: Observable; - - // TODO: Use BitSubject - protected stateSubject: BehaviorSubject = new BehaviorSubject(null); - private stateSubject$ = this.stateSubject.asObservable(); - - state$: Observable; - - // Global: - // FolderService = - - // User (super flat) - // FolderService_{userId}_someData = - // FolderService_{userId}_moreData = - // -- or -- - // User (not as flat) - // FolderService_{userId} = - - constructor( - private stateService: StateService, - private storageLocation: AbstractStorageService, - domainToken: DomainToken) { - - const unformattedKey = `${domainToken.domainName}_{userId}`; - - // startWith? - this.formattedKey$ = this.stateService.activeAccount$ - .pipe( - distinctUntilChanged(), - filter(account => account != null), - map(accountId => unformattedKey.replace("{userId}", accountId)) - ); - - // TODO: Don't use async if possible - const activeAccountData$ = this.formattedKey$ - .pipe(switchMap(async key => { - // TODO: Force this in the storages so I don't have to `as` - const jsonData = await this.storageLocation.get(key) as Jsonify; - const data = domainToken.serializer(jsonData); - return data; - }), - tap(data => this.stateSubject.next(data)), - // Share the execution - share() - ); - - // Whomever subscribes to this data, should be notified of updated data - // if someone calls my update() method, or the active user changes. - this.state$ = defer(() => { - const subscription = activeAccountData$.subscribe(); - return this.stateSubject$ - .pipe(tap({ - complete: () => subscription.unsubscribe(), - })); - }); - } - - async update(configureState: (state: T) => void): Promise { - // wait for lock - try { - const currentState = this.stateSubject.getValue(); - configureState(currentState); - await this.storageLocation.save(await this.createKey(), currentState); - this.stateSubject.next(currentState); - } - finally { - // TODO: Free lock - } - } - - private async createKey(): Promise { - return await firstValueFrom(this.formattedKey$); - } -} - - -export class DefaultActiveUserStateProviderService implements ActiveUserStateProviderService { - private userStateCache: Record> = {}; - - constructor( - private stateService: StateService, // Inject the lightest weight service that provides accountUserId$ - private memoryStorage: AbstractMemoryStorageService, - private diskStorage: AbstractStorageService, - private secureStorage: AbstractStorageService) { - } - - create(location: StorageLocation, domainToken: DomainToken): DefaultActiveUserState { - const locationDomainKey = `${location}_${domainToken.domainName}`; - const existingActiveUserState = this.userStateCache[locationDomainKey]; - if (existingActiveUserState != null) { - // I have to cast out of the unknown generic but this should be safe if rules - // around domain token are made - return existingActiveUserState as DefaultActiveUserState; - } - - const newActiveUserState = new DefaultActiveUserState( - this.stateService, - this.getLocation(location), - domainToken); - this.userStateCache[locationDomainKey] = newActiveUserState; - return newActiveUserState; - } - - private getLocation(location: StorageLocation) { - switch (location) { - case "disk": - return this.diskStorage; - case "secure": - return this.secureStorage; - case "memory": - return this.memoryStorage; - } - } -} diff --git a/libs/common/src/platform/services/state-provider.service.spec.ts b/libs/common/src/platform/services/default-active-user-state.provider.spec.ts similarity index 65% rename from libs/common/src/platform/services/state-provider.service.spec.ts rename to libs/common/src/platform/services/default-active-user-state.provider.spec.ts index ce8ca232a6d..0a404708850 100644 --- a/libs/common/src/platform/services/state-provider.service.spec.ts +++ b/libs/common/src/platform/services/default-active-user-state.provider.spec.ts @@ -1,19 +1,15 @@ -import { matches, mock, mockReset, notNull } from "jest-mock-extended"; +import { matches, mock, mockReset } from "jest-mock-extended"; import { BehaviorSubject } from "rxjs"; import { Jsonify } from "type-fest"; import { StateService } from "../abstractions/state.service" import { AbstractMemoryStorageService } from "../abstractions/storage.service"; +import { KeyDefinition } from "../types/key-definition"; +import { StateDefinition } from "../types/state-definition"; -import { DomainToken, BaseActiveUserStateProviderService } from "./default-global-state-provider.service" - - +import { DefaultActiveUserStateProvider } from "./default-active-user-state.provider"; class TestState { - constructor() { - - } - date: Date; array: string[] // TODO: More complex data types @@ -26,16 +22,18 @@ class TestState { } } -const fakeDomainToken = new DomainToken("fake", TestState.fromJSON); +const testStateDefinition = new StateDefinition("fake", "disk"); -describe("BaseStateProviderService", () => { +const testKeyDefinition = new KeyDefinition(testStateDefinition, "fake", TestState.fromJSON); + +describe("DefaultStateProvider", () => { const stateService = mock(); const memoryStorageService = mock(); const diskStorageService = mock(); const activeAccountSubject = new BehaviorSubject(undefined); - let stateProviderService: BaseActiveUserStateProviderService; + let activeUserStateProvider: DefaultActiveUserStateProvider; beforeEach(() => { mockReset(stateService); @@ -44,7 +42,13 @@ describe("BaseStateProviderService", () => { stateService.activeAccount$ = activeAccountSubject; - stateProviderService = new BaseActiveUserStateProviderService(stateService, diskStorageService); + activeUserStateProvider = new DefaultActiveUserStateProvider( + stateService, + null, // Not testing derived state + null, // Not testing memory storage + diskStorageService, + null // Not testing secure storage + ); }); it("createUserState", async () => { @@ -56,7 +60,7 @@ describe("BaseStateProviderService", () => { return undefined; }); - const fakeDomainState = stateProviderService.create(fakeDomainToken); + const fakeDomainState = activeUserStateProvider.create(testKeyDefinition); const subscribeCallback = jest.fn(); const subscription = fakeDomainState.state$.subscribe(subscribeCallback); @@ -66,8 +70,8 @@ describe("BaseStateProviderService", () => { await new Promise(resolve => setTimeout(resolve, 10)); // Service does an update - fakeDomainState.update(state => state.array.push("value3")); - await new Promise(resolve => setTimeout(resolve, 1)); + await fakeDomainState.update(state => state.array.push("value3")); + await new Promise(resolve => setTimeout(resolve, 10)); subscription.unsubscribe(); @@ -76,13 +80,13 @@ describe("BaseStateProviderService", () => { // Gotten starter user data expect(subscribeCallback).toHaveBeenNthCalledWith(2, matches(value => { - console.log("Called", value); return true; })); // Gotten update callback data expect(subscribeCallback).toHaveBeenNthCalledWith(3, matches((value) => { - return typeof value.date == "object" && + return value != null && + typeof value.date == "object" && value.date.getFullYear() == 2023 && value.array.length == 3 })); diff --git a/libs/common/src/platform/services/default-active-user-state.provider.ts b/libs/common/src/platform/services/default-active-user-state.provider.ts new file mode 100644 index 00000000000..ba0c26a2192 --- /dev/null +++ b/libs/common/src/platform/services/default-active-user-state.provider.ts @@ -0,0 +1,199 @@ +import { BehaviorSubject, Observable, defer, firstValueFrom, map, share, switchMap, tap } from "rxjs"; +import { Jsonify } from "type-fest"; + +import { ActiveUserStateProvider } from "../abstractions/active-user-state.provider"; +import { EncryptService } from "../abstractions/encrypt.service"; +import { StateService } from "../abstractions/state.service"; +import { AbstractMemoryStorageService, AbstractStorageService } from "../abstractions/storage.service"; +import { ActiveUserState } from "../interfaces/active-user-state"; +import { userKeyBuilder } from "../misc/key-builders"; +import { UserKey } from "../models/domain/symmetric-crypto-key"; +import { KeyDefinition } from "../types/key-definition"; + +import { StorageLocation } from "./default-global-state.provider"; + +class ConverterContext { + constructor( + readonly activeUserKey: UserKey, + readonly encryptService: EncryptService + ) { } +} + +class DerivedStateDefinition { + constructor( + readonly converter: (data: TFrom, context: ConverterContext) => Promise + ) { } +} + +export class DerivedActiveUserState { + state$: Observable + + // TODO: Probably needs to take state service + /** + * + */ + constructor( + private derivedStateDefinition: DerivedStateDefinition, + private encryptService: EncryptService, + private activeUserState: ActiveUserState + ) { + this.state$ = activeUserState.state$ + .pipe(switchMap(async from => { + // TODO: How do I get the key? + const convertedData = await derivedStateDefinition.converter(from, new ConverterContext(null, encryptService)); + return convertedData; + })); + } + + async getFromState(): Promise { + const encryptedFromState = await this.activeUserState.getFromState(); + + const context = new ConverterContext(null, this.encryptService); + + const decryptedData = await this.derivedStateDefinition.converter(encryptedFromState, context); + return decryptedData; + } +} + +class DefaultActiveUserState implements ActiveUserState { + private seededInitial = false; + + private formattedKey$: Observable; + private chosenStorageLocation: AbstractStorageService; + + // TODO: Use BitSubject + protected stateSubject: BehaviorSubject = new BehaviorSubject(null); + private stateSubject$ = this.stateSubject.asObservable(); + + state$: Observable; + + constructor( + private keyDefinition: KeyDefinition, + private stateService: StateService, + private encryptService: EncryptService, + private memoryStorageService: AbstractMemoryStorageService, + private secureStorageService: AbstractStorageService, + private diskStorageService: AbstractStorageService + ) { + this.chosenStorageLocation = this.chooseStorage( + this.keyDefinition.stateDefinition.storageLocation + ); + + const unformattedKey = `${this.keyDefinition.stateDefinition.name}_{userId}_${this.keyDefinition.key}`; + + // startWith? + this.formattedKey$ = this.stateService.activeAccount$ + .pipe( + map(accountId => accountId != null + ? unformattedKey.replace("{userId}", accountId) + : null) + ); + + const activeAccountData$ = this.formattedKey$ + .pipe(switchMap(async key => { + console.log("user emitted", key); + if (key == null) { + return null; + } + const jsonData = await this.chosenStorageLocation.get>(key); + const data = keyDefinition.serializer(jsonData); + return data; + }), + tap(data => { + console.log("data:", data); + this.stateSubject.next(data); + }), + // Share the execution + share() + ); + + // Whomever subscribes to this data, should be notified of updated data + // if someone calls my update() method, or the active user changes. + this.state$ = defer(() => { + console.log("starting subscription."); + const subscription = activeAccountData$.subscribe(); + return this.stateSubject$ + .pipe(tap({ + complete: () => subscription.unsubscribe(), + })); + }); + } + + async update(configureState: (state: T) => void): Promise { + const currentState = await firstValueFrom(this.state$); + console.log("data to update:", currentState); + configureState(currentState); + const key = await this.createKey(); + if (key == null) { + throw new Error("Attempting to active user state, when no user is active."); + } + console.log(`updating ${key} to ${currentState}`); + await this.chosenStorageLocation.save(await this.createKey(), currentState); + this.stateSubject.next(currentState); + } + + async getFromState(): Promise { + const activeUserId = await this.stateService.getUserId(); + const key = userKeyBuilder(activeUserId, this.keyDefinition); + const data = await this.chosenStorageLocation.get(key) as Jsonify; + return this.keyDefinition.serializer(data); + } + + createDerived(derivedStateDefinition: DerivedStateDefinition): DerivedActiveUserState { + return new DerivedActiveUserState( + derivedStateDefinition, + this.encryptService, + this + ); + } + + private async createKey(): Promise { + return `${(await firstValueFrom(this.formattedKey$))}`; + } + + private chooseStorage(storageLocation: StorageLocation): AbstractStorageService { + switch (storageLocation) { + case "disk": + return this.diskStorageService; + case "secure": + return this.secureStorageService; + case "memory": + return this.memoryStorageService; + } + } +} + + +export class DefaultActiveUserStateProvider implements ActiveUserStateProvider { + private userStateCache: Record> = {}; + + constructor( + private stateService: StateService, // Inject the lightest weight service that provides accountUserId$ + private encryptService: EncryptService, + private memoryStorage: AbstractMemoryStorageService, + private diskStorage: AbstractStorageService, + private secureStorage: AbstractStorageService) { + } + + create(keyDefinition: KeyDefinition): DefaultActiveUserState { + const locationDomainKey = + `${keyDefinition.stateDefinition.storageLocation}_${keyDefinition.stateDefinition.name}_${keyDefinition.key}`; + const existingActiveUserState = this.userStateCache[locationDomainKey]; + if (existingActiveUserState != null) { + // I have to cast out of the unknown generic but this should be safe if rules + // around domain token are made + return existingActiveUserState as DefaultActiveUserState; + } + + const newActiveUserState = new DefaultActiveUserState( + keyDefinition, + this.stateService, + this.encryptService, + this.memoryStorage, + this.secureStorage, + this.diskStorage + ); + this.userStateCache[locationDomainKey] = newActiveUserState; + return newActiveUserState; + } +} diff --git a/libs/common/src/platform/services/default-global-state-provider.service.ts b/libs/common/src/platform/services/default-global-state-provider.service.ts deleted file mode 100644 index 1399b932224..00000000000 --- a/libs/common/src/platform/services/default-global-state-provider.service.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { BehaviorSubject, Observable, defer, distinctUntilChanged, filter, firstValueFrom, forkJoin, map, merge, share, switchMap, tap } from "rxjs"; -import { Jsonify } from "type-fest"; - -import { ActiveUserStateProviderService } from "../abstractions/active-user-state-provider.service"; -import { GlobalStateProviderService } from "../abstractions/global-state-provider.service"; -import { StateService } from "../abstractions/state.service"; -import { AbstractMemoryStorageService, AbstractStorageService } from "../abstractions/storage.service"; -import { State } from "../interfaces/state"; - -// TODO: Move type -// TODO: How can we protect the creation of these so that platform can maintain the allowed creations? -export class DomainToken { - constructor( - public domainName: string, - public serializer: (jsonData: Jsonify) => T) { - - } -} - -// TODO: Move type -export type StorageLocation = "memory" | "disk" | "secure"; - -class DefaultGlobalState implements State { - protected stateSubject: BehaviorSubject = new BehaviorSubject(null); - - state$: Observable; - - constructor(private storageLocation: AbstractStorageService, private domainToken: DomainToken) { - this.state$ = this.stateSubject.asObservable(); - } - - async update(configureState: (state: T) => void): Promise { - // wait for lock - try { - const currentState = this.stateSubject.getValue(); - configureState(currentState); - await this.storageLocation.save(this.domainToken.domainName, currentState); - this.stateSubject.next(currentState); - } - finally { - // TODO: Free lock - } - } -} - -export class DefaultGlobalStateProviderService implements GlobalStateProviderService { - private globalStateCache: Record> = {}; - - constructor( - private memoryStorage: AbstractMemoryStorageService, - private diskStorage: AbstractStorageService, - private secureStorage: AbstractStorageService) { - } - - create(location: StorageLocation, domainToken: DomainToken): DefaultGlobalState { - const locationDomainKey = `${location}_${domainToken.domainName}`; - const existingGlobalState = this.globalStateCache[locationDomainKey]; - if (existingGlobalState != null) { - // I have to cast out of the unknown generic but this should be safe if rules - // around domain token are made - return existingGlobalState as DefaultGlobalState; - } - - - const newGlobalState = new DefaultGlobalState(this.getLocation(location), domainToken); - - this.globalStateCache[locationDomainKey] = newGlobalState; - return newGlobalState; - } - - private getLocation(location: StorageLocation) { - switch (location) { - case "disk": - return this.diskStorage; - case "secure": - return this.secureStorage; - case "memory": - return this.memoryStorage; - } - } -} diff --git a/libs/common/src/platform/services/default-global-state.provider.ts b/libs/common/src/platform/services/default-global-state.provider.ts new file mode 100644 index 00000000000..4fc7054883a --- /dev/null +++ b/libs/common/src/platform/services/default-global-state.provider.ts @@ -0,0 +1,84 @@ +import { BehaviorSubject, Observable, defer, firstValueFrom } from "rxjs"; + +import { GlobalStateProvider } from "../abstractions/global-state.provider"; +import { AbstractMemoryStorageService, AbstractStorageService } from "../abstractions/storage.service"; +import { ActiveUserState } from "../interfaces/active-user-state"; +import { KeyDefinition } from "../types/key-definition"; +import { Jsonify } from "type-fest"; +import { globalKeyBuilder } from "../misc/key-builders"; + + + +// TODO: Move type +export type StorageLocation = "memory" | "disk" | "secure"; + +// class DefaultGlobalState implements ActiveUserState { +// private storageKey: string; + +// protected stateSubject: BehaviorSubject = new BehaviorSubject(null); + +// state$: Observable; + +// constructor( +// private keyDefinition: KeyDefinition, +// private chosenLocation: AbstractStorageService +// ) { +// this.storageKey = globalKeyBuilder(this.keyDefinition); + +// // TODO: When subsribed to, we need to read data from the chosen storage location +// // and give it back +// this.state$ = new Observable() +// } + +// async update(configureState: (state: T) => void): Promise { +// const currentState = await firstValueFrom(this.state$); +// configureState(currentState); +// await this.chosenLocation.save(this.storageKey, currentState); +// this.stateSubject.next(currentState); +// } + +// async getFromState(): Promise { +// const data = await this.chosenLocation.get>(this.storageKey); +// return this.keyDefinition.serializer(data); +// } +// } + +// export class DefaultGlobalStateProvider implements GlobalStateProvider { +// private globalStateCache: Record> = {}; + +// constructor( +// private memoryStorage: AbstractMemoryStorageService, +// private diskStorage: AbstractStorageService, +// private secureStorage: AbstractStorageService) { +// } + +// create(keyDefinition: KeyDefinition): DefaultGlobalState { +// const locationDomainKey = `${keyDefinition.stateDefinition.storageLocation}_${keyDefinition.stateDefinition.name}_${keyDefinition.key}`; +// const existingGlobalState = this.globalStateCache[locationDomainKey]; +// if (existingGlobalState != null) { +// // I have to cast out of the unknown generic but this should be safe if rules +// // around domain token are made +// return existingGlobalState as DefaultGlobalState; +// } + + +// const newGlobalState = new DefaultGlobalState( +// keyDefinition, +// this.getLocation(keyDefinition.stateDefinition.storageLocation) +// ); + +// this.globalStateCache[locationDomainKey] = newGlobalState; +// return newGlobalState; +// } + +// private getLocation(location: StorageLocation) { +// switch (location) { +// case "disk": +// return this.diskStorage; +// case "secure": +// return this.secureStorage; +// case "memory": +// return this.memoryStorage; +// } +// } +// } diff --git a/libs/common/src/platform/types/derived-state-definition.ts b/libs/common/src/platform/types/derived-state-definition.ts new file mode 100644 index 00000000000..acd9a1770b3 --- /dev/null +++ b/libs/common/src/platform/types/derived-state-definition.ts @@ -0,0 +1,18 @@ +import { EncryptService } from "../abstractions/encrypt.service"; +import { UserKey } from "../models/domain/symmetric-crypto-key"; +import { StorageLocation } from "../services/default-global-state.provider"; + +// TODO: Move type +export class DeriveContext { + constructor( + readonly activeUserKey: UserKey, + readonly encryptService: EncryptService + ) { } +} + +export class DerivedStateDefinition { + constructor( + readonly location: StorageLocation, + readonly converter: (data: TFrom, context: DeriveContext) => Promise + ) {} +} diff --git a/libs/common/src/platform/types/key-definition.ts b/libs/common/src/platform/types/key-definition.ts new file mode 100644 index 00000000000..c340c1eebcf --- /dev/null +++ b/libs/common/src/platform/types/key-definition.ts @@ -0,0 +1,52 @@ +import { Jsonify } from "type-fest"; + +import { DeriveContext, DerivedStateDefinition } from "./derived-state-definition"; +import { StateDefinition, StorageLocation } from "./state-definition"; + +/** + * + */ +// TODO Import Purpose type +export class KeyDefinition { + /** + * Creates a new instance of a KeyDefinition + * @param stateDefinition The state definition for which this key belongs to. + * @param key The name of the key, this should be unique per domain + * @param serializer A function to use to safely convert your type from json to your expected type. + */ + constructor( + readonly stateDefinition: StateDefinition, + readonly key: string, + readonly serializer: (jsonValue: Jsonify) => T + ) { } + + static array(stateDefinition: StateDefinition, key: string, serializer: (jsonValue: Jsonify) => T) { + return new KeyDefinition(stateDefinition, key, (jsonValue) => { + // TODO: Should we handle null for them, I feel like we should discourage null for an array? + return jsonValue.map(v => serializer(v)); + }); + } + + static record(stateDefinition: StateDefinition, key: string, serializer: (jsonValue: Jsonify) => T) { + return new KeyDefinition>(stateDefinition, key, (jsonValue) => { + const output: Record = {}; + for (const key in jsonValue) { + output[key] = serializer((jsonValue as Record>)[key]); + } + return output; + }); + } + + /** + * Helper for defining a derived definition that will often be used alongside a given key + * @param storageLocation + * @param decrypt + * @returns + */ + createDerivedDefinition( + storageLocation: StorageLocation, + decrypt: (data: T, context: DeriveContext) => Promise + ) { + return new DerivedStateDefinition(storageLocation, decrypt); + } +} diff --git a/libs/common/src/platform/types/state-definition.ts b/libs/common/src/platform/types/state-definition.ts new file mode 100644 index 00000000000..2f870e7f03f --- /dev/null +++ b/libs/common/src/platform/types/state-definition.ts @@ -0,0 +1,19 @@ +// TODO: How can we protect the creation of these so that platform can maintain the allowed creations? + +// TODO: Where should this live +export type StorageLocation = "disk" | "memory" | "secure"; + +/** + * + */ +export class StateDefinition { + /** + * + * @param name The name of the state, this needs to be unique from all other {@link StateDefinition}'s. + * @param storageLocation The location of where this state should be stored. + */ + constructor( + readonly name: string, + readonly storageLocation: StorageLocation + ) { } +} diff --git a/libs/common/src/platform/types/state-definitions.spec.ts b/libs/common/src/platform/types/state-definitions.spec.ts new file mode 100644 index 00000000000..735da54287f --- /dev/null +++ b/libs/common/src/platform/types/state-definitions.spec.ts @@ -0,0 +1,22 @@ +import { StateDefinition } from "./state-definition"; +import * as definitions from "./state-definitions"; + +it("has all unique definitions", () => { + const uniqueNames: string[] = []; + const keys = Object.keys(definitions); + + for (const key of keys) { + const definition = (definitions as unknown as Record)[key]; + if (Object.getPrototypeOf(definition) !== StateDefinition.prototype) { + throw new Error(`${key} from import ./state-definitions is expected to be a StateDefinition but wasn't.`); + } + + const name = `${definition.name}_${definition.storageLocation}`; + + if (uniqueNames.includes(name)) { + throw new Error(`Definition ${key} is invalid, it's elements have already been claimed. Please choose a unique name.`); + } + + uniqueNames.push(name); + } +}); diff --git a/libs/common/src/platform/types/state-definitions.ts b/libs/common/src/platform/types/state-definitions.ts new file mode 100644 index 00000000000..3fda37149d1 --- /dev/null +++ b/libs/common/src/platform/types/state-definitions.ts @@ -0,0 +1,3 @@ +import { StateDefinition } from "./state-definition"; + +export const FOLDER_SERVICE_DISK = new StateDefinition("FolderService", "disk"); diff --git a/libs/common/src/state-migrations/migrate.ts b/libs/common/src/state-migrations/migrate.ts index 483c4f2e8eb..305f4d1c7f5 100644 --- a/libs/common/src/state-migrations/migrate.ts +++ b/libs/common/src/state-migrations/migrate.ts @@ -11,10 +11,11 @@ import { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org- import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key"; import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account"; import { MoveStateVersionMigrator } from "./migrations/8-move-state-version"; +import { MoveFolderToOwnedMigrator } from "./migrations/9-move-folder-to-owned"; import { MinVersionMigrator } from "./migrations/min-version"; export const MIN_VERSION = 2; -export const CURRENT_VERSION = 8; +export const CURRENT_VERSION = 9; export type MinVersion = typeof MIN_VERSION; export async function migrate( @@ -38,7 +39,8 @@ export async function migrate( .with(AddKeyTypeToOrgKeysMigrator, 4, 5) .with(RemoveLegacyEtmKeyMigrator, 5, 6) .with(MoveBiometricAutoPromptToAccount, 6, 7) - .with(MoveStateVersionMigrator, 7, CURRENT_VERSION) + .with(MoveStateVersionMigrator, 7, 8) + .with(MoveFolderToOwnedMigrator, 8, CURRENT_VERSION) .migrate(migrationHelper); } diff --git a/libs/common/src/state-migrations/migrations/9-move-folder-to-owned.ts b/libs/common/src/state-migrations/migrations/9-move-folder-to-owned.ts new file mode 100644 index 00000000000..37af1722f6d --- /dev/null +++ b/libs/common/src/state-migrations/migrations/9-move-folder-to-owned.ts @@ -0,0 +1,52 @@ +// TODO: Add message +// eslint-disable-next-line import/no-restricted-paths +import { userKeyBuilder } from "../../platform/misc/key-builders"; +// TODO: Add message +// eslint-disable-next-line import/no-restricted-paths +import { KeyDefinition } from "../../platform/types/key-definition"; +// TODO: Add message +// eslint-disable-next-line import/no-restricted-paths +import { StateDefinition } from "../../platform/types/state-definition"; +import { MigrationHelper } from "../migration-helper"; +import { IRREVERSIBLE, Migrator } from "../migrator"; + +type ExpectedAccountType = { + data: { + folders: { + encrypted: Record + } + } +}; + +const FOLDER_STATE = new StateDefinition("FolderService", "disk"); + +const INITIAL_FOLDER_USER_KEY = new KeyDefinition(FOLDER_STATE, "folders", (s) => s); + +export class MoveFolderToOwnedMigrator extends Migrator<8, 9> { + async migrate(helper: MigrationHelper): Promise { + const accounts = await helper.getAccounts(); + async function updateAccount(userId: string, account: ExpectedAccountType) { + if (account == null) { + return; + } + + const userKey = userKeyBuilder(userId, INITIAL_FOLDER_USER_KEY); + await helper.set(userKey, account.data.folders.encrypted); + + // TODO: Is there ever anything more on the folders object than the encrypted prop + // delete account.data.folders; + helper.info(`Would delete: ${JSON.stringify(account.data.folders)}`); + // TODO: + // await helper.set("", account); + } + + await Promise.all( + accounts.map(({ userId, account}) => updateAccount(userId, account)) + ); + } + + rollback(helper: MigrationHelper): Promise { + // TODO: This doesn't actually need to be irreversible + throw IRREVERSIBLE; + } +} diff --git a/libs/common/src/state-migrations/owned-migrator.ts b/libs/common/src/state-migrations/owned-migrator.ts deleted file mode 100644 index 44e02001a24..00000000000 --- a/libs/common/src/state-migrations/owned-migrator.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { LogService } from "../platform/abstractions/log.service"; -import { DomainToken } from "../platform/services/default-global-state-provider.service"; - -import { MigrationHelper } from "./migration-helper"; -import { Migrator } from "./migrator"; - -export class OwnedMigrationHelper { - - currentVersion: number; - logService: LogService; - - constructor( - public domainToken: DomainToken, - private migrationHelper: MigrationHelper) { - this.currentVersion = migrationHelper.currentVersion; - this.logService = migrationHelper.logService; - } - - get(key: string): Promise { - return this.migrationHelper.get(key); - } - - getFromOwned(key: string): Promise { - - } - - set(key: string, value: T): Promise { - // Create the key - return undefined; - } - info(message: string): void { - // Add domain info? - this.migrationHelper.info("$OwnedMigratonHelper: {message}"); - } - - getAccounts(): Promise<{ userId: string; account: ExpectedAccountType; }[]> { - return this.migrationHelper.getAccounts(); - } -} - -export abstract class OwnedMigrator extends Migrator { - constructor(public domainToken: DomainToken, - fromVersion: TFrom, toVersion: TTo) { - super(fromVersion, toVersion) - } - - override async migrate(helper: MigrationHelper): Promise { - // Create our custom helper - const ownedHelper = new OwnedMigrationHelper(this.domainToken, helper); - await this.migrateOwned(ownedHelper); - const accounts = await ownedHelper.getAccounts(); - for (const account of accounts) { - await this.migrateUserOwned(account.userId, ownedHelper); - } - } - - abstract migrateOwned(helper: OwnedMigrationHelper): Promise; - abstract migrateUserOwned(userId: string, helper: OwnedMigrationHelper): Promise; -} diff --git a/libs/common/src/vault/services/folder/folder.service.ts b/libs/common/src/vault/services/folder/folder.service.ts index 3a562601bf7..733839fbdf0 100644 --- a/libs/common/src/vault/services/folder/folder.service.ts +++ b/libs/common/src/vault/services/folder/folder.service.ts @@ -1,66 +1,57 @@ -import { BehaviorSubject, concatMap } from "rxjs"; -import { Jsonify } from "type-fest"; +import { Observable, firstValueFrom, map } from "rxjs"; +import { ActiveUserStateProvider } from "../../../platform/abstractions/active-user-state.provider"; import { CryptoService } from "../../../platform/abstractions/crypto.service"; import { I18nService } from "../../../platform/abstractions/i18n.service"; import { StateService } from "../../../platform/abstractions/state.service"; +import { ActiveUserState } from "../../../platform/interfaces/active-user-state"; import { Utils } from "../../../platform/misc/utils"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; -import { ActiveUserStateProviderService, DomainToken, State } from "../../../platform/services/default-global-state-provider.service"; +import { DerivedActiveUserState } from "../../../platform/services/default-active-user-state.provider"; import { CipherService } from "../../../vault/abstractions/cipher.service"; import { InternalFolderService as InternalFolderServiceAbstraction } from "../../../vault/abstractions/folder/folder.service.abstraction"; import { CipherData } from "../../../vault/models/data/cipher.data"; import { FolderData } from "../../../vault/models/data/folder.data"; import { Folder } from "../../../vault/models/domain/folder"; import { FolderView } from "../../../vault/models/view/folder.view"; +import { FOLDERS } from "../../types/key-definitions"; -class UserFolderState { - static fromJSON(jsonState: Jsonify) { - const state = new UserFolderState(); - return state; - } -} export class FolderService implements InternalFolderServiceAbstraction { - protected _folders: BehaviorSubject = new BehaviorSubject([]); - protected _folderViews: BehaviorSubject = new BehaviorSubject([]); - folders$ = this._folders.asObservable(); - folderViews$ = this._folderViews.asObservable(); + folderState: ActiveUserState>; + decryptedFolderState: DerivedActiveUserState, FolderView[]> - private folderState: State; + folders$: Observable; + folderViews$: Observable; constructor( private cryptoService: CryptoService, private i18nService: I18nService, private cipherService: CipherService, - private stateService: StateService, - private activeUserStateProviderService: ActiveUserStateProviderService + private activeUserStateProvider: ActiveUserStateProvider, + private stateService: StateService ) { - this.folderState = activeUserStateProviderService.create(new DomainToken("folder", UserFolderState.fromJSON)); - this.stateService.activeAccountUnlocked$ - .pipe( - concatMap(async (unlocked) => { - if (Utils.global.bitwardenContainerService == null) { - return; - } + const derivedFoldersDefinition = FOLDERS.createDerivedDefinition("memory", async (foldersMap) => { + const folders = this.flattenMap(foldersMap); + const decryptedFolders = await this.decryptFolders(folders); + return decryptedFolders; + }) - if (!unlocked) { - this._folders.next([]); - this._folderViews.next([]); - return; - } + this.folderState = this.activeUserStateProvider.create(FOLDERS); - const data = await this.stateService.getEncryptedFolders(); + this.folders$ = this.folderState.state$ + .pipe(map(foldersMap => { + return this.flattenMap(foldersMap); + })); - await this.updateObservables(data); - }) - ) - .subscribe(); + this.decryptedFolderState = this.folderState.createDerived(derivedFoldersDefinition); + this.folderViews$ = this.decryptedFolderState.state$; } async clearCache(): Promise { - this._folderViews.next([]); + // TODO: I don't really have a replacement for this right now + // this._folderViews.next([]); } // TODO: This should be moved to EncryptService or something @@ -72,21 +63,13 @@ export class FolderService implements InternalFolderServiceAbstraction { } async get(id: string): Promise { - const folders = this._folders.getValue(); - - return folders.find((folder) => folder.id === id); + const folders = await firstValueFrom(this.folderState.state$); + return folders[id]; } async getAllFromState(): Promise { - const folders = await this.stateService.getEncryptedFolders(); - const response: Folder[] = []; - for (const id in folders) { - // eslint-disable-next-line - if (folders.hasOwnProperty(id)) { - response.push(new Folder(folders[id])); - } - } - return response; + const foldersMap = await this.folderState.getFromState(); + return this.flattenMap(foldersMap); } /** @@ -94,76 +77,58 @@ export class FolderService implements InternalFolderServiceAbstraction { * @param id id of the folder */ async getFromState(id: string): Promise { - const foldersMap = await this.stateService.getEncryptedFolders(); + const foldersMap = await this.folderState.getFromState(); const folder = foldersMap[id]; if (folder == null) { return null; } - return new Folder(folder); + return folder; } /** * @deprecated Only use in CLI! */ async getAllDecryptedFromState(): Promise { - const data = await this.stateService.getEncryptedFolders(); - const folders = Object.values(data || {}).map((f) => new Folder(f)); - - return this.decryptFolders(folders); + return await this.decryptedFolderState.getFromState(); } async upsert(folder: FolderData | FolderData[]): Promise { - let folders = await this.stateService.getEncryptedFolders(); - if (folders == null) { - folders = {}; - } - - if (folder instanceof FolderData) { - const f = folder as FolderData; - folders[f.id] = f; - } else { - (folder as FolderData[]).forEach((f) => { - folders[f.id] = f; - }); - } - - await this.updateObservables(folders); - await this.stateService.setEncryptedFolders(folders); + console.log("upsert", folder); + await this.folderState.update(folders => { + if (folder instanceof FolderData) { + const f = folder as FolderData; + folders[f.id] = new Folder(f); + } else { + (folder as FolderData[]).forEach((f) => { + folders[f.id] = new Folder(f); + }); + } + }); } async replace(folders: { [id: string]: FolderData }): Promise { - await this.updateObservables(folders); - await this.stateService.setEncryptedFolders(folders); + const convertedFolders = Object.entries(folders).reduce((agg, [key, value]) => { + agg[key] = new Folder(value); + return agg; + }, {} as Record); + console.log("replace", folders, convertedFolders); + await this.folderState.update(f => f = convertedFolders); } async clear(userId?: string): Promise { - if (userId == null || userId == (await this.stateService.getUserId())) { - this._folders.next([]); - this._folderViews.next([]); - } - await this.stateService.setEncryptedFolders(null, { userId: userId }); + console.log("clear", userId); + await this.folderState.update(f => f = null); } - async delete(id: string | string[]): Promise { - const folders = await this.stateService.getEncryptedFolders(); - if (folders == null) { - return; - } - - if (typeof id === "string") { - if (folders[id] == null) { - return; + async delete(id: string | string[]): Promise { + const folderIds = typeof id === "string" ? [id] : id; + console.log("delete", folderIds); + await this.folderState.update(folders => { + for (const folderId in folderIds) { + delete folders[folderId]; } - delete folders[id]; - } else { - (id as string[]).forEach((i) => { - delete folders[i]; - }); - } - - await this.updateObservables(folders); - await this.stateService.setEncryptedFolders(folders); + }); // Items in a deleted folder are re-assigned to "No Folder" const ciphers = await this.stateService.getEncryptedCiphers(); @@ -181,16 +146,6 @@ export class FolderService implements InternalFolderServiceAbstraction { } } - private async updateObservables(foldersMap: { [id: string]: FolderData }) { - const folders = Object.values(foldersMap || {}).map((f) => new Folder(f)); - - this._folders.next(folders); - - if (await this.cryptoService.hasUserKey()) { - this._folderViews.next(await this.decryptFolders(folders)); - } - } - private async decryptFolders(folders: Folder[]) { const decryptFolderPromises = folders.map((f) => f.decrypt()); const decryptedFolders = await Promise.all(decryptFolderPromises); @@ -203,4 +158,12 @@ export class FolderService implements InternalFolderServiceAbstraction { return decryptedFolders; } + + private flattenMap(foldersMap: Record): Folder[] { + const folders: Folder[] = []; + for (const id in foldersMap) { + folders.push(foldersMap[id]); + } + return folders; + } } diff --git a/libs/common/src/vault/types/key-definitions.ts b/libs/common/src/vault/types/key-definitions.ts new file mode 100644 index 00000000000..9f7ee283c4d --- /dev/null +++ b/libs/common/src/vault/types/key-definitions.ts @@ -0,0 +1,6 @@ +import { KeyDefinition } from "../../platform/types/key-definition"; +import { FOLDER_SERVICE_DISK } from "../../platform/types/state-definitions"; +import { Folder } from "../models/domain/folder"; + +// FolderService Keys +export const FOLDERS = KeyDefinition.record(FOLDER_SERVICE_DISK, "folders", Folder.fromJSON);