From 5bca53736bd238be5febbe0d1a6bdf20ad2c1438 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karol=20S=C3=B3jko?= Date: Fri, 11 Aug 2023 08:59:16 +0200 Subject: [PATCH] chore: display shared vault file usage (#2399) * chore: display shared vault file usage * fix: specs * fix: reshape filtering result * fix: resolving invalid server items * fix: get revisions specs * fix: processing issue * fix: tests --------- Co-authored-by: Mo --- ...-core-npm-1.25.0-51a2ed924b-f99196f620.zip | Bin 0 -> 69057 bytes packages/api/package.json | 2 +- packages/desktop/package.json | 2 +- packages/encryption/package.json | 2 +- .../src/Domain/Keys/ItemsKey/ItemsKey.ts | 2 +- .../KeySystemItemsKey/KeySystemItemsKey.ts | 2 +- .../src/Domain/Keys/RootKey/Functions.ts | 2 +- .../src/Domain/Keys/RootKey/RootKey.ts | 2 +- .../Domain/Operator/004/Operator004.spec.ts | 9 +- .../KeySystem/CreateKeySystemItemsKey.ts | 2 +- .../KeySystem/CreateRandomKeySystemRootKey.ts | 2 +- .../CreateUserInputKeySystemRootKey.ts | 2 +- .../KeySystem/DeriveKeySystemRootKey.ts | 2 +- .../004/UseCase/RootKey/DeriveRootKey.ts | 3 +- .../Symmetric/GenerateAuthenticatedData.ts | 2 +- .../Symmetric/GenerateEncryptedParameters.ts | 2 +- .../GenerateEncryptedProtocolString.spec.ts | 3 +- .../GenerateEncryptedProtocolString.ts | 2 +- .../Operator/EncryptionOperatorsInterface.ts | 2 +- .../src/Domain/Operator/Functions.ts | 2 +- .../src/Domain/Types/EncryptedParameters.ts | 8 +- .../src/Domain/Types/ItemAuthenticatedData.ts | 2 +- packages/features/package.json | 2 +- packages/models/package.json | 2 +- .../Abstract/Contextual/FilteredServerItem.ts | 40 +++++++-- .../Abstract/Item/Interfaces/EncryptedItem.ts | 2 +- .../Implementations/EncryptedPayload.ts | 10 ++- .../Payload/Interfaces/EncryptedPayload.ts | 2 +- .../Interfaces/TypeCheck.spec.ts | 2 +- .../TransferPayload/Interfaces/TypeCheck.ts | 4 +- .../src/Domain/Local/ApplicationIdentifier.ts | 1 + .../KeySystemRootKeyParamsInterface.ts | 2 +- .../Domain/Local/Protocol/ProtocolVersion.ts | 47 ++++++++++ .../ProtocolVersionFromEncryptedString.ts | 15 ++++ .../Domain/Local/RootKey/RootKeyInterface.ts | 2 +- .../AsymmetricMessageSharedVaultInvite.ts | 1 + .../Syncable/ItemsKey/ItemsKeyInterface.ts | 2 +- .../KeySystemItemsKeyContent.ts | 2 +- .../KeySystemItemsKeyInterface.ts | 2 +- .../KeySystemRootKey/KeySystemRootKey.ts | 2 +- .../KeySystemRootKeyContent.ts | 2 +- .../KeySystemRootKeyInterface.ts | 2 +- .../VaultListing/VaultListingSharingInfo.ts | 1 + packages/models/src/Domain/index.ts | 78 ++++++++--------- .../responses/src/Domain/Item/ConflictType.ts | 1 + .../SharedVaults/SharedVaultServerHash.ts | 4 + packages/services/package.json | 2 +- .../Application/ApplicationInterface.ts | 3 +- .../Application/Options/RequiredOptions.ts | 3 +- .../ProcessAcceptedVaultInvite.spec.ts | 1 + .../UseCase/ProcessAcceptedVaultInvite.ts | 1 + .../src/Domain/Device/DeviceInterface.ts | 2 +- .../UseCase/Asymmetric/DecryptMessage.spec.ts | 2 +- .../Domain/ItemsEncryption/ItemsEncryption.ts | 2 +- .../Domain/Session/SessionsClientInterface.ts | 3 +- .../SharedVaults/SharedVaultService.spec.ts | 6 ++ .../Domain/SharedVaults/SharedVaultService.ts | 20 ++++- .../SharedVaults/SharedVaultServiceEvent.ts | 1 + .../UseCase/ConvertToSharedVault.ts | 1 + .../SharedVaults/UseCase/CreateSharedVault.ts | 1 + ...cLocalVaultsWithRemoteSharedVaults.spec.ts | 80 ++++++++++++++++++ .../SyncLocalVaultsWithRemoteSharedVaults.ts | 40 +++++++++ .../services/src/Domain/Strings/Messages.ts | 2 +- .../VaultInvite/UseCase/InviteToVault.ts | 1 + packages/services/src/Domain/index.ts | 1 + .../Application/Dependencies/Dependencies.ts | 9 ++ .../lib/Application/Dependencies/Types.ts | 1 + .../ApplicationGroup/ApplicationDescriptor.ts | 2 +- .../UseCase/GetRevision/GetRevision.spec.ts | 8 +- .../Domain/UseCase/GetRevision/GetRevision.ts | 5 +- .../snjs/lib/Migrations/MigrationServices.ts | 3 +- .../Migrations/StorageReaders/Functions.ts | 3 +- .../lib/Migrations/StorageReaders/Reader.ts | 3 +- .../lib/Services/Sync/Account/Response.ts | 56 +++++++++--- .../Services/Sync/Account/ResponseResolver.ts | 1 + .../lib/Services/Sync/Account/Utilities.ts | 36 ++++---- .../snjs/lib/Services/Sync/SyncService.ts | 20 ++++- packages/snjs/lib/index.ts | 2 +- packages/snjs/mocha/lib/factory.js | 7 +- packages/snjs/mocha/payload.test.js | 2 +- packages/snjs/mocha/sync_tests/online.test.js | 7 +- packages/snjs/package.json | 2 +- packages/ui-services/package.json | 2 +- .../Preferences/Panes/Vaults/Vaults.tsx | 8 ++ .../Panes/Vaults/Vaults/VaultItem.tsx | 11 ++- .../Event/ApplicationEventObserver.spec.ts | 2 +- yarn.lock | 27 ++++-- 87 files changed, 505 insertions(+), 169 deletions(-) create mode 100644 .yarn/cache/@standardnotes-domain-core-npm-1.25.0-51a2ed924b-f99196f620.zip create mode 100644 packages/models/src/Domain/Local/ApplicationIdentifier.ts create mode 100644 packages/models/src/Domain/Local/Protocol/ProtocolVersion.ts create mode 100644 packages/models/src/Domain/Local/Protocol/ProtocolVersionFromEncryptedString.ts create mode 100644 packages/services/src/Domain/SharedVaults/UseCase/SyncLocalVaultsWithRemoteSharedVaults.spec.ts create mode 100644 packages/services/src/Domain/SharedVaults/UseCase/SyncLocalVaultsWithRemoteSharedVaults.ts diff --git a/.yarn/cache/@standardnotes-domain-core-npm-1.25.0-51a2ed924b-f99196f620.zip b/.yarn/cache/@standardnotes-domain-core-npm-1.25.0-51a2ed924b-f99196f620.zip new file mode 100644 index 0000000000000000000000000000000000000000..aa20e6ae09b3503d0c171c33a5edbd804270a89f GIT binary patch literal 69057 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%0AEeW8Sg3{8)q5PzHA6Llhw&| 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 z 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 impl constructor(rawPayload: EncryptedTransferPayload, source = PayloadSource.Constructor) { super(rawPayload, source) + const versionResult = ProtocolVersionFromEncryptedString(rawPayload.content) + if (versionResult.isFailed()) { + throw new Error('EncryptedPayload constructor versionResult is failed') + } + this.auth_hash = rawPayload.auth_hash this.content = rawPayload.content this.deleted = false this.enc_item_key = rawPayload.enc_item_key this.errorDecrypting = rawPayload.errorDecrypting this.items_key_id = rawPayload.items_key_id - this.version = protocolVersionFromEncryptedString(this.content) + this.version = versionResult.getValue() this.waitingForKey = rawPayload.waitingForKey } diff --git a/packages/models/src/Domain/Abstract/Payload/Interfaces/EncryptedPayload.ts b/packages/models/src/Domain/Abstract/Payload/Interfaces/EncryptedPayload.ts index 33c86621e..0448a093b 100644 --- a/packages/models/src/Domain/Abstract/Payload/Interfaces/EncryptedPayload.ts +++ b/packages/models/src/Domain/Abstract/Payload/Interfaces/EncryptedPayload.ts @@ -1,4 +1,4 @@ -import { ProtocolVersion } from '@standardnotes/common' +import { ProtocolVersion } from '../../../Local/Protocol/ProtocolVersion' import { EncryptedTransferPayload } from '../../TransferPayload/Interfaces/EncryptedTransferPayload' import { PayloadInterface } from './PayloadInterface' diff --git a/packages/models/src/Domain/Abstract/TransferPayload/Interfaces/TypeCheck.spec.ts b/packages/models/src/Domain/Abstract/TransferPayload/Interfaces/TypeCheck.spec.ts index 9234b150a..02f93e3dd 100644 --- a/packages/models/src/Domain/Abstract/TransferPayload/Interfaces/TypeCheck.spec.ts +++ b/packages/models/src/Domain/Abstract/TransferPayload/Interfaces/TypeCheck.spec.ts @@ -42,7 +42,7 @@ describe('type check', () => { expect( isCorruptTransferPayload({ uuid: '123', - content_type: ContentType.TYPES.Unknown, + content_type: 'Unknown', content: '123', ...PayloadTimestampDefaults(), }), diff --git a/packages/models/src/Domain/Abstract/TransferPayload/Interfaces/TypeCheck.ts b/packages/models/src/Domain/Abstract/TransferPayload/Interfaces/TypeCheck.ts index 9466a6d96..b8c96e168 100644 --- a/packages/models/src/Domain/Abstract/TransferPayload/Interfaces/TypeCheck.ts +++ b/packages/models/src/Domain/Abstract/TransferPayload/Interfaces/TypeCheck.ts @@ -26,5 +26,7 @@ export function isDeletedTransferPayload(payload: TransferPayload): payload is D export function isCorruptTransferPayload(payload: TransferPayload): boolean { const invalidDeletedState = payload.deleted === true && payload.content != undefined - return payload.uuid == undefined || invalidDeletedState || payload.content_type === ContentType.TYPES.Unknown + const contenTypeOrError = ContentType.create(payload.content_type) + + return payload.uuid == undefined || invalidDeletedState || contenTypeOrError.isFailed() } diff --git a/packages/models/src/Domain/Local/ApplicationIdentifier.ts b/packages/models/src/Domain/Local/ApplicationIdentifier.ts new file mode 100644 index 000000000..20c51e9e3 --- /dev/null +++ b/packages/models/src/Domain/Local/ApplicationIdentifier.ts @@ -0,0 +1 @@ +export type ApplicationIdentifier = string diff --git a/packages/models/src/Domain/Local/KeyParams/KeySystemRootKeyParamsInterface.ts b/packages/models/src/Domain/Local/KeyParams/KeySystemRootKeyParamsInterface.ts index e8211d61a..5d0a985e4 100644 --- a/packages/models/src/Domain/Local/KeyParams/KeySystemRootKeyParamsInterface.ts +++ b/packages/models/src/Domain/Local/KeyParams/KeySystemRootKeyParamsInterface.ts @@ -1,5 +1,5 @@ -import { ProtocolVersion } from '@standardnotes/common' import { KeySystemIdentifier } from '../../Syncable/KeySystemRootKey/KeySystemIdentifier' +import { ProtocolVersion } from '../Protocol/ProtocolVersion' import { KeySystemPasswordType } from './KeySystemPasswordType' /** diff --git a/packages/models/src/Domain/Local/Protocol/ProtocolVersion.ts b/packages/models/src/Domain/Local/Protocol/ProtocolVersion.ts new file mode 100644 index 000000000..58f40c0aa --- /dev/null +++ b/packages/models/src/Domain/Local/Protocol/ProtocolVersion.ts @@ -0,0 +1,47 @@ +export enum ProtocolVersion { + V001 = '001', + V002 = '002', + V003 = '003', + V004 = '004', +} + +export const ProtocolVersionLatest = ProtocolVersion.V004 + +/** The last protocol version to not use root-key based items keys */ +export const ProtocolVersionLastNonrootItemsKey = ProtocolVersion.V003 + +export const ProtocolExpirationDates: Partial> = Object.freeze({ + [ProtocolVersion.V001]: Date.parse('2018-01-01'), + [ProtocolVersion.V002]: Date.parse('2020-01-01'), +}) + +export function isProtocolVersionExpired(version: ProtocolVersion) { + const expireDate = ProtocolExpirationDates[version] + if (!expireDate) { + return false + } + + const expired = new Date().getTime() > expireDate + return expired +} + +export const ProtocolVersionLength = 3 + +/** + * -1 if a < b + * 0 if a == b + * 1 if a > b + */ +export function compareVersions(a: ProtocolVersion, b: ProtocolVersion): number { + const aNum = Number(a) + const bNum = Number(b) + return aNum - bNum +} + +export function leftVersionGreaterThanOrEqualToRight(a: ProtocolVersion, b: ProtocolVersion): boolean { + return compareVersions(a, b) >= 0 +} + +export function isVersionLessThanOrEqualTo(input: ProtocolVersion, compareTo: ProtocolVersion): boolean { + return compareVersions(input, compareTo) <= 0 +} diff --git a/packages/models/src/Domain/Local/Protocol/ProtocolVersionFromEncryptedString.ts b/packages/models/src/Domain/Local/Protocol/ProtocolVersionFromEncryptedString.ts new file mode 100644 index 000000000..1c77e8bc8 --- /dev/null +++ b/packages/models/src/Domain/Local/Protocol/ProtocolVersionFromEncryptedString.ts @@ -0,0 +1,15 @@ +import { Result } from '@standardnotes/domain-core' +import { ProtocolVersion, ProtocolVersionLength } from './ProtocolVersion' + +export function ProtocolVersionFromEncryptedString(string: string): Result { + try { + const version = string.substring(0, ProtocolVersionLength) as ProtocolVersion + if (Object.values(ProtocolVersion).includes(version)) { + return Result.ok(version) + } + } catch (error) { + return Result.fail(JSON.stringify(error)) + } + + return Result.fail(`Invalid encrypted string ${string}`) +} diff --git a/packages/models/src/Domain/Local/RootKey/RootKeyInterface.ts b/packages/models/src/Domain/Local/RootKey/RootKeyInterface.ts index 58ab73b94..9655c7b63 100644 --- a/packages/models/src/Domain/Local/RootKey/RootKeyInterface.ts +++ b/packages/models/src/Domain/Local/RootKey/RootKeyInterface.ts @@ -1,9 +1,9 @@ import { PkcKeyPair } from '@standardnotes/sncrypto-common' -import { ProtocolVersion } from '@standardnotes/common' import { DecryptedItemInterface } from '../../Abstract/Item/Interfaces/DecryptedItem' import { RootKeyParamsInterface } from '../KeyParams/RootKeyParamsInterface' import { NamespacedRootKeyInKeychain, RootKeyContentInStorage } from './KeychainTypes' import { RootKeyContent } from './RootKeyContent' +import { ProtocolVersion } from '../Protocol/ProtocolVersion' export interface RootKeyInterface extends DecryptedItemInterface { readonly keyParams: RootKeyParamsInterface diff --git a/packages/models/src/Domain/Runtime/AsymmetricMessage/MessageTypes/AsymmetricMessageSharedVaultInvite.ts b/packages/models/src/Domain/Runtime/AsymmetricMessage/MessageTypes/AsymmetricMessageSharedVaultInvite.ts index 9091dcdfb..e459c277e 100644 --- a/packages/models/src/Domain/Runtime/AsymmetricMessage/MessageTypes/AsymmetricMessageSharedVaultInvite.ts +++ b/packages/models/src/Domain/Runtime/AsymmetricMessage/MessageTypes/AsymmetricMessageSharedVaultInvite.ts @@ -19,6 +19,7 @@ export type AsymmetricMessageSharedVaultInvite = { name: string description?: string iconString: IconType | EmojiString + fileBytesUsed: number } } } diff --git a/packages/models/src/Domain/Syncable/ItemsKey/ItemsKeyInterface.ts b/packages/models/src/Domain/Syncable/ItemsKey/ItemsKeyInterface.ts index 6271147e9..3104f4c1c 100644 --- a/packages/models/src/Domain/Syncable/ItemsKey/ItemsKeyInterface.ts +++ b/packages/models/src/Domain/Syncable/ItemsKey/ItemsKeyInterface.ts @@ -1,6 +1,6 @@ -import { ProtocolVersion } from '@standardnotes/common' import { DecryptedItemInterface } from './../../Abstract/Item/Interfaces/DecryptedItem' import { ItemContent, SpecializedContent } from '../../Abstract/Content/ItemContent' +import { ProtocolVersion } from '../../Local/Protocol/ProtocolVersion' export interface ItemsKeyContentSpecialized extends SpecializedContent { version: ProtocolVersion diff --git a/packages/models/src/Domain/Syncable/KeySystemItemsKey/KeySystemItemsKeyContent.ts b/packages/models/src/Domain/Syncable/KeySystemItemsKey/KeySystemItemsKeyContent.ts index 7fb19d50c..dbda3af83 100644 --- a/packages/models/src/Domain/Syncable/KeySystemItemsKey/KeySystemItemsKeyContent.ts +++ b/packages/models/src/Domain/Syncable/KeySystemItemsKey/KeySystemItemsKeyContent.ts @@ -1,5 +1,5 @@ -import { ProtocolVersion } from '@standardnotes/common' import { ItemContent, SpecializedContent } from '../../Abstract/Content/ItemContent' +import { ProtocolVersion } from '../../Local/Protocol/ProtocolVersion' export interface KeySystemItemsKeyContentSpecialized extends SpecializedContent { version: ProtocolVersion diff --git a/packages/models/src/Domain/Syncable/KeySystemItemsKey/KeySystemItemsKeyInterface.ts b/packages/models/src/Domain/Syncable/KeySystemItemsKey/KeySystemItemsKeyInterface.ts index 46d7c23e2..9781a7e50 100644 --- a/packages/models/src/Domain/Syncable/KeySystemItemsKey/KeySystemItemsKeyInterface.ts +++ b/packages/models/src/Domain/Syncable/KeySystemItemsKey/KeySystemItemsKeyInterface.ts @@ -1,5 +1,5 @@ -import { ProtocolVersion } from '@standardnotes/common' import { DecryptedItemInterface } from '../../Abstract/Item/Interfaces/DecryptedItem' +import { ProtocolVersion } from '../../Local/Protocol/ProtocolVersion' import { KeySystemItemsKeyContent } from './KeySystemItemsKeyContent' export interface KeySystemItemsKeyInterface extends DecryptedItemInterface { diff --git a/packages/models/src/Domain/Syncable/KeySystemRootKey/KeySystemRootKey.ts b/packages/models/src/Domain/Syncable/KeySystemRootKey/KeySystemRootKey.ts index 39d5893e7..57dce30f3 100644 --- a/packages/models/src/Domain/Syncable/KeySystemRootKey/KeySystemRootKey.ts +++ b/packages/models/src/Domain/Syncable/KeySystemRootKey/KeySystemRootKey.ts @@ -1,4 +1,3 @@ -import { ProtocolVersion } from '@standardnotes/common' import { ConflictStrategy, DecryptedItem } from '../../Abstract/Item' import { DecryptedPayloadInterface } from '../../Abstract/Payload' import { HistoryEntryInterface } from '../../Runtime/History' @@ -7,6 +6,7 @@ import { KeySystemRootKeyInterface } from './KeySystemRootKeyInterface' import { KeySystemIdentifier } from './KeySystemIdentifier' import { KeySystemRootKeyParamsInterface } from '../../Local/KeyParams/KeySystemRootKeyParamsInterface' import { ContentType } from '@standardnotes/domain-core' +import { ProtocolVersion } from '../../Local/Protocol/ProtocolVersion' export function isKeySystemRootKey(x: { content_type: string }): x is KeySystemRootKey { return x.content_type === ContentType.TYPES.KeySystemRootKey diff --git a/packages/models/src/Domain/Syncable/KeySystemRootKey/KeySystemRootKeyContent.ts b/packages/models/src/Domain/Syncable/KeySystemRootKey/KeySystemRootKeyContent.ts index 4c370dec0..5c0904a5b 100644 --- a/packages/models/src/Domain/Syncable/KeySystemRootKey/KeySystemRootKeyContent.ts +++ b/packages/models/src/Domain/Syncable/KeySystemRootKey/KeySystemRootKeyContent.ts @@ -1,7 +1,7 @@ -import { ProtocolVersion } from '@standardnotes/common' import { ItemContent } from '../../Abstract/Content/ItemContent' import { KeySystemIdentifier } from './KeySystemIdentifier' import { KeySystemRootKeyParamsInterface } from '../../Local/KeyParams/KeySystemRootKeyParamsInterface' +import { ProtocolVersion } from '../../Local/Protocol/ProtocolVersion' export type KeySystemRootKeyContentSpecialized = { keyParams: KeySystemRootKeyParamsInterface diff --git a/packages/models/src/Domain/Syncable/KeySystemRootKey/KeySystemRootKeyInterface.ts b/packages/models/src/Domain/Syncable/KeySystemRootKey/KeySystemRootKeyInterface.ts index 0b5e3ef9f..b4b137269 100644 --- a/packages/models/src/Domain/Syncable/KeySystemRootKey/KeySystemRootKeyInterface.ts +++ b/packages/models/src/Domain/Syncable/KeySystemRootKey/KeySystemRootKeyInterface.ts @@ -1,8 +1,8 @@ -import { ProtocolVersion } from '@standardnotes/common' import { DecryptedItemInterface } from '../../Abstract/Item/Interfaces/DecryptedItem' import { KeySystemRootKeyContent } from './KeySystemRootKeyContent' import { KeySystemIdentifier } from './KeySystemIdentifier' import { KeySystemRootKeyParamsInterface } from '../../Local/KeyParams/KeySystemRootKeyParamsInterface' +import { ProtocolVersion } from '../../Local/Protocol/ProtocolVersion' export interface KeySystemRootKeyInterface extends DecryptedItemInterface { keyParams: KeySystemRootKeyParamsInterface diff --git a/packages/models/src/Domain/Syncable/VaultListing/VaultListingSharingInfo.ts b/packages/models/src/Domain/Syncable/VaultListing/VaultListingSharingInfo.ts index f35657e32..93725b326 100644 --- a/packages/models/src/Domain/Syncable/VaultListing/VaultListingSharingInfo.ts +++ b/packages/models/src/Domain/Syncable/VaultListing/VaultListingSharingInfo.ts @@ -1,4 +1,5 @@ export type VaultListingSharingInfo = { sharedVaultUuid: string ownerUserUuid: string + fileBytesUsed: number } diff --git a/packages/models/src/Domain/index.ts b/packages/models/src/Domain/index.ts index d552db119..c4caf0ddb 100644 --- a/packages/models/src/Domain/index.ts +++ b/packages/models/src/Domain/index.ts @@ -15,7 +15,6 @@ export * from './Abstract/Contextual/ComponentCreate' export * from './Abstract/Contextual/ComponentRetrieved' export * from './Abstract/Contextual/ContextPayload' export * from './Abstract/Contextual/FilteredServerItem' -export * from './Abstract/Contextual/TrustedConflictParams' export * from './Abstract/Contextual/Functions' export * from './Abstract/Contextual/LocalStorage' export * from './Abstract/Contextual/OfflineSyncPush' @@ -23,30 +22,33 @@ export * from './Abstract/Contextual/OfflineSyncSaved' export * from './Abstract/Contextual/ServerSyncPush' export * from './Abstract/Contextual/ServerSyncSaved' export * from './Abstract/Contextual/SessionHistory' +export * from './Abstract/Contextual/TrustedConflictParams' export * from './Abstract/Item' export * from './Abstract/Payload' export * from './Abstract/TransferPayload' - export * from './Api/Subscription/Invitation' export * from './Api/Subscription/InvitationStatus' export * from './Api/Subscription/InviteeIdentifierType' export * from './Api/Subscription/InviterIdentifierType' - export * from './Device/Environment' export * from './Device/Platform' - -export * from './Local/KeyParams/RootKeyParamsInterface' -export * from './Local/KeyParams/KeySystemRootKeyParamsInterface' +export * from './Local/ApplicationIdentifier' export * from './Local/KeyParams/KeySystemPasswordType' +export * from './Local/KeyParams/KeySystemRootKeyParamsInterface' +export * from './Local/KeyParams/RootKeyParamsInterface' +export * from './Local/Protocol/ProtocolVersion' +export * from './Local/Protocol/ProtocolVersionFromEncryptedString' export * from './Local/RootKey/KeychainTypes' export * from './Local/RootKey/RootKeyContent' export * from './Local/RootKey/RootKeyInterface' export * from './Local/RootKey/RootKeyWithKeyPairsInterface' - -export * from './Runtime/Feature/TypeGuards' -export * from './Runtime/Feature/UIFeature' -export * from './Runtime/Feature/UIFeatureInterface' - +export * from './Runtime/AsymmetricMessage/AsymmetricMessagePayload' +export * from './Runtime/AsymmetricMessage/AsymmetricMessagePayloadType' +export * from './Runtime/AsymmetricMessage/MessageTypes/AsymmetricMessageSenderKeypairChanged' +export * from './Runtime/AsymmetricMessage/MessageTypes/AsymmetricMessageSharedVaultInvite' +export * from './Runtime/AsymmetricMessage/MessageTypes/AsymmetricMessageSharedVaultMetadataChanged' +export * from './Runtime/AsymmetricMessage/MessageTypes/AsymmetricMessageSharedVaultRootKeyChanged' +export * from './Runtime/AsymmetricMessage/MessageTypes/AsymmetricMessageTrustedContactShare' export * from './Runtime/Collection/CollectionSort' export * from './Runtime/Collection/Item/ItemCollection' export * from './Runtime/Collection/Item/ItemCounter' @@ -57,6 +59,13 @@ export * from './Runtime/DirtyCounter/DirtyCounter' export * from './Runtime/Display' export * from './Runtime/Display/ItemDisplayController' export * from './Runtime/Display/Types' +export * from './Runtime/Encryption/ContentTypesUsingRootKeyEncryption' +export * from './Runtime/Encryption/ContentTypeUsesKeySystemRootKeyEncryption' +export * from './Runtime/Encryption/ContentTypeUsesRootKeyEncryption' +export * from './Runtime/Encryption/PersistentSignatureData' +export * from './Runtime/Feature/TypeGuards' +export * from './Runtime/Feature/UIFeature' +export * from './Runtime/Feature/UIFeatureInterface' export * from './Runtime/History' export * from './Runtime/Index/ItemDelta' export * from './Runtime/Index/SNIndex' @@ -69,20 +78,6 @@ export * from './Runtime/Predicate/NotPredicate' export * from './Runtime/Predicate/Operator' export * from './Runtime/Predicate/Predicate' export * from './Runtime/Predicate/Utils' - -export * from './Runtime/AsymmetricMessage/AsymmetricMessagePayload' -export * from './Runtime/AsymmetricMessage/AsymmetricMessagePayloadType' -export * from './Runtime/AsymmetricMessage/MessageTypes/AsymmetricMessageSenderKeypairChanged' -export * from './Runtime/AsymmetricMessage/MessageTypes/AsymmetricMessageSharedVaultInvite' -export * from './Runtime/AsymmetricMessage/MessageTypes/AsymmetricMessageSharedVaultMetadataChanged' -export * from './Runtime/AsymmetricMessage/MessageTypes/AsymmetricMessageSharedVaultRootKeyChanged' -export * from './Runtime/AsymmetricMessage/MessageTypes/AsymmetricMessageTrustedContactShare' - -export * from './Runtime/Encryption/PersistentSignatureData' -export * from './Runtime/Encryption/ContentTypeUsesRootKeyEncryption' -export * from './Runtime/Encryption/ContentTypesUsingRootKeyEncryption' -export * from './Runtime/Encryption/ContentTypeUsesKeySystemRootKeyEncryption' - export * from './Syncable/ActionsExtension' export * from './Syncable/Component' export * from './Syncable/Editor' @@ -90,36 +85,32 @@ export * from './Syncable/FeatureRepo' export * from './Syncable/File' export * from './Syncable/ItemsKey/ItemsKeyInterface' export * from './Syncable/ItemsKey/ItemsKeyMutatorInterface' +export * from './Syncable/KeySystemItemsKey/KeySystemItemsKeyContent' +export * from './Syncable/KeySystemItemsKey/KeySystemItemsKeyInterface' +export * from './Syncable/KeySystemItemsKey/KeySystemItemsKeyMutatorInterface' +export * from './Syncable/KeySystemRootKey/KeySystemIdentifier' +export * from './Syncable/KeySystemRootKey/KeySystemRootKey' +export * from './Syncable/KeySystemRootKey/KeySystemRootKeyContent' +export * from './Syncable/KeySystemRootKey/KeySystemRootKeyInterface' +export * from './Syncable/KeySystemRootKey/KeySystemRootKeyMutator' +export * from './Syncable/KeySystemRootKey/KeySystemRootKeyStorageMode' export * from './Syncable/Note' export * from './Syncable/SmartView' export * from './Syncable/Tag' -export * from './Syncable/UserPrefs' - -export * from './Syncable/TrustedContact/TrustedContact' -export * from './Syncable/TrustedContact/Mutator/TrustedContactMutator' export * from './Syncable/TrustedContact/Content/TrustedContactContent' -export * from './Syncable/TrustedContact/TrustedContactInterface' -export * from './Syncable/TrustedContact/PublicKeySet/ContactPublicKeySetInterface' +export * from './Syncable/TrustedContact/Mutator/TrustedContactMutator' export * from './Syncable/TrustedContact/PublicKeySet/ContactPublicKeySet' +export * from './Syncable/TrustedContact/PublicKeySet/ContactPublicKeySetInterface' +export * from './Syncable/TrustedContact/TrustedContact' +export * from './Syncable/TrustedContact/TrustedContactInterface' export * from './Syncable/TrustedContact/Types/PortablePublicKeySet' export * from './Syncable/TrustedContact/Types/PublicKeyTrustStatus' - -export * from './Syncable/KeySystemRootKey/KeySystemRootKey' -export * from './Syncable/KeySystemRootKey/KeySystemRootKeyMutator' -export * from './Syncable/KeySystemRootKey/KeySystemRootKeyContent' -export * from './Syncable/KeySystemRootKey/KeySystemRootKeyInterface' -export * from './Syncable/KeySystemRootKey/KeySystemRootKeyStorageMode' - -export * from './Syncable/KeySystemItemsKey/KeySystemItemsKeyInterface' -export * from './Syncable/KeySystemItemsKey/KeySystemItemsKeyContent' -export * from './Syncable/KeySystemItemsKey/KeySystemItemsKeyMutatorInterface' - +export * from './Syncable/UserPrefs' export * from './Syncable/VaultListing/VaultListing' export * from './Syncable/VaultListing/VaultListingContent' export * from './Syncable/VaultListing/VaultListingInterface' export * from './Syncable/VaultListing/VaultListingMutator' export * from './Syncable/VaultListing/VaultListingSharingInfo' - export * from './Utilities/Icon/IconType' export * from './Utilities/Item/FindItem' export * from './Utilities/Item/ItemContentsDiffer' @@ -132,4 +123,3 @@ export * from './Utilities/Payload/PayloadContentsEqual' export * from './Utilities/Payload/PayloadsByAlternatingUuid' export * from './Utilities/Payload/PayloadsByDuplicating' export * from './Utilities/Payload/PayloadSplit' -export * from './Syncable/KeySystemRootKey/KeySystemIdentifier' diff --git a/packages/responses/src/Domain/Item/ConflictType.ts b/packages/responses/src/Domain/Item/ConflictType.ts index c3f80a782..534f6510d 100644 --- a/packages/responses/src/Domain/Item/ConflictType.ts +++ b/packages/responses/src/Domain/Item/ConflictType.ts @@ -5,6 +5,7 @@ export enum ConflictType { ContentError = 'content_error', ReadOnlyError = 'readonly_error', UuidError = 'uuid_error', + InvalidServerItem = 'invalid_server_item', SharedVaultSnjsVersionError = 'shared_vault_snjs_version_error', SharedVaultInsufficientPermissionsError = 'shared_vault_insufficient_permissions_error', diff --git a/packages/responses/src/Domain/SharedVaults/SharedVaultServerHash.ts b/packages/responses/src/Domain/SharedVaults/SharedVaultServerHash.ts index aefd70529..0134b5ffb 100644 --- a/packages/responses/src/Domain/SharedVaults/SharedVaultServerHash.ts +++ b/packages/responses/src/Domain/SharedVaults/SharedVaultServerHash.ts @@ -1,4 +1,8 @@ export interface SharedVaultServerHash { uuid: string user_uuid: string + file_upload_bytes_used: number + file_upload_bytes_limit: number + created_at_timestamp: number + updated_at_timestamp: number } diff --git a/packages/services/package.json b/packages/services/package.json index 25ad01321..8e2f8dc01 100644 --- a/packages/services/package.json +++ b/packages/services/package.json @@ -18,7 +18,7 @@ "dependencies": { "@standardnotes/api": "workspace:^", "@standardnotes/common": "^1.50.0", - "@standardnotes/domain-core": "^1.24.0", + "@standardnotes/domain-core": "^1.25.0", "@standardnotes/encryption": "workspace:^", "@standardnotes/features": "workspace:^", "@standardnotes/files": "workspace:^", diff --git a/packages/services/src/Domain/Application/ApplicationInterface.ts b/packages/services/src/Domain/Application/ApplicationInterface.ts index 81c712661..28c145686 100644 --- a/packages/services/src/Domain/Application/ApplicationInterface.ts +++ b/packages/services/src/Domain/Application/ApplicationInterface.ts @@ -22,8 +22,7 @@ import { AsymmetricMessageServiceInterface } from './../AsymmetricMessage/Asymme import { ImportDataResult } from '../Import/ImportDataResult' import { ChallengeServiceInterface } from './../Challenge/ChallengeServiceInterface' import { VaultServiceInterface } from '../Vault/VaultServiceInterface' -import { ApplicationIdentifier } from '@standardnotes/common' -import { BackupFile, Environment, Platform, PrefKey, PrefValue } from '@standardnotes/models' +import { BackupFile, Environment, Platform, PrefKey, PrefValue, ApplicationIdentifier } from '@standardnotes/models' import { BackupServiceInterface, FilesClientInterface } from '@standardnotes/files' import { AlertService } from '../Alert/AlertService' diff --git a/packages/services/src/Domain/Application/Options/RequiredOptions.ts b/packages/services/src/Domain/Application/Options/RequiredOptions.ts index bd4702aff..99bf22bbc 100644 --- a/packages/services/src/Domain/Application/Options/RequiredOptions.ts +++ b/packages/services/src/Domain/Application/Options/RequiredOptions.ts @@ -1,5 +1,4 @@ -import { Environment, Platform } from '@standardnotes/models' -import { ApplicationIdentifier } from '@standardnotes/common' +import { Environment, Platform, ApplicationIdentifier } from '@standardnotes/models' import { AlertService, DeviceInterface } from '@standardnotes/services' import { PureCryptoInterface } from '@standardnotes/sncrypto-common' diff --git a/packages/services/src/Domain/AsymmetricMessage/UseCase/ProcessAcceptedVaultInvite.spec.ts b/packages/services/src/Domain/AsymmetricMessage/UseCase/ProcessAcceptedVaultInvite.spec.ts index a7a52f25f..33f8f882f 100644 --- a/packages/services/src/Domain/AsymmetricMessage/UseCase/ProcessAcceptedVaultInvite.spec.ts +++ b/packages/services/src/Domain/AsymmetricMessage/UseCase/ProcessAcceptedVaultInvite.spec.ts @@ -38,6 +38,7 @@ describe('ProcessAcceptedVaultInvite', () => { metadata: { name: 'test-name', iconString: 'safe-square', + fileBytesUsed: 0, }, trustedContacts: [], }, diff --git a/packages/services/src/Domain/AsymmetricMessage/UseCase/ProcessAcceptedVaultInvite.ts b/packages/services/src/Domain/AsymmetricMessage/UseCase/ProcessAcceptedVaultInvite.ts index b3fa74e4e..110abc2da 100644 --- a/packages/services/src/Domain/AsymmetricMessage/UseCase/ProcessAcceptedVaultInvite.ts +++ b/packages/services/src/Domain/AsymmetricMessage/UseCase/ProcessAcceptedVaultInvite.ts @@ -36,6 +36,7 @@ export class ProcessAcceptedVaultInvite { sharing: { sharedVaultUuid: sharedVaultUuid, ownerUserUuid: ownerUuid, + fileBytesUsed: metadata.fileBytesUsed, }, } diff --git a/packages/services/src/Domain/Device/DeviceInterface.ts b/packages/services/src/Domain/Device/DeviceInterface.ts index ca0bf6d93..ff1948684 100644 --- a/packages/services/src/Domain/Device/DeviceInterface.ts +++ b/packages/services/src/Domain/Device/DeviceInterface.ts @@ -1,5 +1,5 @@ -import { ApplicationIdentifier } from '@standardnotes/common' import { + ApplicationIdentifier, FullyFormedTransferPayload, TransferPayload, NamespacedRootKeyInKeychain, diff --git a/packages/services/src/Domain/Encryption/UseCase/Asymmetric/DecryptMessage.spec.ts b/packages/services/src/Domain/Encryption/UseCase/Asymmetric/DecryptMessage.spec.ts index 95ff45158..607b24e2a 100644 --- a/packages/services/src/Domain/Encryption/UseCase/Asymmetric/DecryptMessage.spec.ts +++ b/packages/services/src/Domain/Encryption/UseCase/Asymmetric/DecryptMessage.spec.ts @@ -3,10 +3,10 @@ import { ContactPublicKeySetInterface, PublicKeyTrustStatus, TrustedContactInterface, + ProtocolVersion, } from '@standardnotes/models' import { DecryptMessage } from './DecryptMessage' import { OperatorInterface, EncryptionOperatorsInterface } from '@standardnotes/encryption' -import { ProtocolVersion } from '@standardnotes/common' function createMockPublicKeySetChain(): ContactPublicKeySetInterface { const nMinusOne = new ContactPublicKeySet({ diff --git a/packages/services/src/Domain/ItemsEncryption/ItemsEncryption.ts b/packages/services/src/Domain/ItemsEncryption/ItemsEncryption.ts index 059e35cf3..9bbe2579c 100644 --- a/packages/services/src/Domain/ItemsEncryption/ItemsEncryption.ts +++ b/packages/services/src/Domain/ItemsEncryption/ItemsEncryption.ts @@ -1,5 +1,4 @@ import { FindDefaultItemsKey } from './../Encryption/UseCase/ItemsKey/FindDefaultItemsKey' -import { ProtocolVersion } from '@standardnotes/common' import { DecryptedParameters, ErrorDecryptingParameters, @@ -23,6 +22,7 @@ import { PayloadEmitSource, KeySystemItemsKeyInterface, SureFindPayload, + ProtocolVersion, ContentTypeUsesRootKeyEncryption, } from '@standardnotes/models' import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface' diff --git a/packages/services/src/Domain/Session/SessionsClientInterface.ts b/packages/services/src/Domain/Session/SessionsClientInterface.ts index fe87cdf3c..5c1e4e688 100644 --- a/packages/services/src/Domain/Session/SessionsClientInterface.ts +++ b/packages/services/src/Domain/Session/SessionsClientInterface.ts @@ -1,7 +1,6 @@ import { UserRegistrationResponseBody } from '@standardnotes/api' -import { ProtocolVersion } from '@standardnotes/common' import { SNRootKey } from '@standardnotes/encryption' -import { RootKeyInterface } from '@standardnotes/models' +import { RootKeyInterface, ProtocolVersion } from '@standardnotes/models' import { SessionBody, SignInResponse, diff --git a/packages/services/src/Domain/SharedVaults/SharedVaultService.spec.ts b/packages/services/src/Domain/SharedVaults/SharedVaultService.spec.ts index 1d02300d0..d5da6bb8d 100644 --- a/packages/services/src/Domain/SharedVaults/SharedVaultService.spec.ts +++ b/packages/services/src/Domain/SharedVaults/SharedVaultService.spec.ts @@ -14,9 +14,11 @@ import { SyncServiceInterface } from '../Sync/SyncServiceInterface' import { ItemManagerInterface } from '../Item/ItemManagerInterface' import { SessionsClientInterface } from '../Session/SessionsClientInterface' import { ContactPublicKeySetInterface, TrustedContactInterface } from '@standardnotes/models' +import { SyncLocalVaultsWithRemoteSharedVaults } from './UseCase/SyncLocalVaultsWithRemoteSharedVaults' describe('SharedVaultService', () => { let service: SharedVaultService + let syncLocalVaultsWithRemoteSharedVaults: SyncLocalVaultsWithRemoteSharedVaults beforeEach(() => { const sync = {} as jest.Mocked @@ -37,12 +39,16 @@ describe('SharedVaultService', () => { const deleteSharedVaultUseCase = {} as jest.Mocked const discardItemsLocally = {} as jest.Mocked + syncLocalVaultsWithRemoteSharedVaults = {} as jest.Mocked + syncLocalVaultsWithRemoteSharedVaults.execute = jest.fn() + const eventBus = {} as jest.Mocked eventBus.addEventHandler = jest.fn() service = new SharedVaultService( items, session, + syncLocalVaultsWithRemoteSharedVaults, getVault, getOwnedVaults, createSharedVaultUseCase, diff --git a/packages/services/src/Domain/SharedVaults/SharedVaultService.ts b/packages/services/src/Domain/SharedVaults/SharedVaultService.ts index 92ac05301..08a1ff0c8 100644 --- a/packages/services/src/Domain/SharedVaults/SharedVaultService.ts +++ b/packages/services/src/Domain/SharedVaults/SharedVaultService.ts @@ -32,6 +32,7 @@ import { ContentType, NotificationType, Uuid } from '@standardnotes/domain-core' import { HandleKeyPairChange } from '../Contacts/UseCase/HandleKeyPairChange' import { FindContact } from '../Contacts/UseCase/FindContact' import { GetOwnedSharedVaults } from './UseCase/GetOwnedSharedVaults' +import { SyncLocalVaultsWithRemoteSharedVaults } from './UseCase/SyncLocalVaultsWithRemoteSharedVaults' export class SharedVaultService extends AbstractService @@ -40,6 +41,7 @@ export class SharedVaultService constructor( private items: ItemManagerInterface, private session: SessionsClientInterface, + private _syncLocalVaultsWithRemoteSharedVaults: SyncLocalVaultsWithRemoteSharedVaults, private _getVault: GetVault, private _getOwnedSharedVaults: GetOwnedSharedVaults, private _createSharedVault: CreateSharedVault, @@ -67,6 +69,7 @@ export class SharedVaultService super.deinit() ;(this.items as unknown) = undefined ;(this.session as unknown) = undefined + ;(this._syncLocalVaultsWithRemoteSharedVaults as unknown) = undefined ;(this._getVault as unknown) = undefined ;(this._createSharedVault as unknown) = undefined ;(this._handleKeyPairChange as unknown) = undefined @@ -88,7 +91,7 @@ export class SharedVaultService break } case NotificationServiceEvent.NotificationReceived: - await this.handleUserEvent(event.payload as NotificationServiceEventPayload) + await this.handleNotification(event.payload as NotificationServiceEventPayload) break case SyncEvent.ReceivedRemoteSharedVaults: void this.notifyEventSync(SharedVaultServiceEvent.SharedVaultStatusChanged) @@ -96,7 +99,7 @@ export class SharedVaultService } } - private async handleUserEvent(event: NotificationServiceEventPayload): Promise { + private async handleNotification(event: NotificationServiceEventPayload): Promise { switch (event.eventPayload.props.type.value) { case NotificationType.TYPES.RemovedFromSharedVault: { const vault = this._getVault.execute({ @@ -114,6 +117,19 @@ export class SharedVaultService } break } + case NotificationType.TYPES.SharedVaultFileRemoved: + case NotificationType.TYPES.SharedVaultFileUploaded: { + const vaultOrError = this._getVault.execute({ + sharedVaultUuid: event.eventPayload.props.sharedVaultUuid.value, + }) + if (!vaultOrError.isFailed()) { + await this._syncLocalVaultsWithRemoteSharedVaults.execute([vaultOrError.getValue()]) + + void this.notifyEventSync(SharedVaultServiceEvent.SharedVaultStatusChanged) + } + + break + } } } diff --git a/packages/services/src/Domain/SharedVaults/SharedVaultServiceEvent.ts b/packages/services/src/Domain/SharedVaults/SharedVaultServiceEvent.ts index ac210e91f..4f91e79eb 100644 --- a/packages/services/src/Domain/SharedVaults/SharedVaultServiceEvent.ts +++ b/packages/services/src/Domain/SharedVaults/SharedVaultServiceEvent.ts @@ -2,6 +2,7 @@ import { KeySystemIdentifier } from '@standardnotes/models' export enum SharedVaultServiceEvent { SharedVaultStatusChanged = 'SharedVaultStatusChanged', + SharedVaultFileStorageUsageChanged = 'SharedVaultFileStorageUsageChanged', } export type SharedVaultServiceEventPayload = { diff --git a/packages/services/src/Domain/SharedVaults/UseCase/ConvertToSharedVault.ts b/packages/services/src/Domain/SharedVaults/UseCase/ConvertToSharedVault.ts index c75c897c8..63812bb0a 100644 --- a/packages/services/src/Domain/SharedVaults/UseCase/ConvertToSharedVault.ts +++ b/packages/services/src/Domain/SharedVaults/UseCase/ConvertToSharedVault.ts @@ -31,6 +31,7 @@ export class ConvertToSharedVault { mutator.sharing = { sharedVaultUuid: serverVaultHash.uuid, ownerUserUuid: serverVaultHash.user_uuid, + fileBytesUsed: serverVaultHash.file_upload_bytes_used, } }, ) diff --git a/packages/services/src/Domain/SharedVaults/UseCase/CreateSharedVault.ts b/packages/services/src/Domain/SharedVaults/UseCase/CreateSharedVault.ts index 97decf859..a9cb7baaf 100644 --- a/packages/services/src/Domain/SharedVaults/UseCase/CreateSharedVault.ts +++ b/packages/services/src/Domain/SharedVaults/UseCase/CreateSharedVault.ts @@ -50,6 +50,7 @@ export class CreateSharedVault { mutator.sharing = { sharedVaultUuid: serverVaultHash.uuid, ownerUserUuid: serverVaultHash.user_uuid, + fileBytesUsed: serverVaultHash.file_upload_bytes_used, } }, ) diff --git a/packages/services/src/Domain/SharedVaults/UseCase/SyncLocalVaultsWithRemoteSharedVaults.spec.ts b/packages/services/src/Domain/SharedVaults/UseCase/SyncLocalVaultsWithRemoteSharedVaults.spec.ts new file mode 100644 index 000000000..dff7e572a --- /dev/null +++ b/packages/services/src/Domain/SharedVaults/UseCase/SyncLocalVaultsWithRemoteSharedVaults.spec.ts @@ -0,0 +1,80 @@ +import { MutatorClientInterface, SharedVaultServerInterface, VaultListingInterface } from '@standardnotes/snjs' +import { SyncLocalVaultsWithRemoteSharedVaults } from './SyncLocalVaultsWithRemoteSharedVaults' + +describe('SyncLocalVaultsWithRemoteSharedVaults', () => { + let sharedVaultServer: SharedVaultServerInterface + let mutator: MutatorClientInterface + + const createUseCase = () => new SyncLocalVaultsWithRemoteSharedVaults(sharedVaultServer, mutator) + + beforeEach(() => { + sharedVaultServer = {} as jest.Mocked + sharedVaultServer.getSharedVaults = jest.fn().mockResolvedValue({ data: { sharedVaults: [{ + uuid: '1-2-3', + user_uuid: '2-3-4', + file_upload_bytes_used: 123, + file_upload_bytes_limit: 10000000, + created_at_timestamp: 123, + updated_at_timestamp: 123, + }] } }) + + mutator = {} as jest.Mocked + mutator.changeItem = jest.fn() + }) + + it('should sync local vaults with remote shared vaults to update file storage usage', async () => { + const localVaults = [{ + uuid: '1-2-3', + name: 'Vault', + isSharedVaultListing: () => true, + sharing: { + sharedVaultUuid: '1-2-3', + ownerUserUuid: '2-3-4', + fileBytesUsed: 0, + }, + } as jest.Mocked] + + const useCase = createUseCase() + await useCase.execute(localVaults) + + expect(mutator.changeItem).toHaveBeenCalledWith(localVaults[0], expect.any(Function)) + }) + + it('should fail if shared vault server returns error', async () => { + sharedVaultServer.getSharedVaults = jest.fn().mockResolvedValue({ data: { error: { message: 'test-error' } } }) + + const localVaults = [{ + uuid: '1-2-3', + name: 'Vault', + isSharedVaultListing: () => true, + sharing: { + sharedVaultUuid: '1-2-3', + ownerUserUuid: '2-3-4', + fileBytesUsed: 0, + }, + } as jest.Mocked] + + const useCase = createUseCase() + const result = await useCase.execute(localVaults) + + expect(result.isFailed()).toBe(true) + }) + + it('should not sync local vaults with remote shared vaults if local vault is not shared', async () => { + const localVaults = [{ + uuid: '1-2-3', + name: 'Vault', + isSharedVaultListing: () => false, + sharing: { + sharedVaultUuid: '1-2-3', + ownerUserUuid: '2-3-4', + fileBytesUsed: 0, + }, + } as jest.Mocked] + + const useCase = createUseCase() + await useCase.execute(localVaults) + + expect(mutator.changeItem).not.toHaveBeenCalled() + }) +}) diff --git a/packages/services/src/Domain/SharedVaults/UseCase/SyncLocalVaultsWithRemoteSharedVaults.ts b/packages/services/src/Domain/SharedVaults/UseCase/SyncLocalVaultsWithRemoteSharedVaults.ts new file mode 100644 index 000000000..2769a23a3 --- /dev/null +++ b/packages/services/src/Domain/SharedVaults/UseCase/SyncLocalVaultsWithRemoteSharedVaults.ts @@ -0,0 +1,40 @@ +import { Result, UseCaseInterface } from '@standardnotes/domain-core' +import { SharedVaultServerInterface } from '@standardnotes/api' +import { HttpError, isErrorResponse } from '@standardnotes/responses' +import { SharedVaultListingInterface, VaultListingInterface, VaultListingMutator } from '@standardnotes/models' + +import { MutatorClientInterface } from '../../Mutator/MutatorClientInterface' + +export class SyncLocalVaultsWithRemoteSharedVaults implements UseCaseInterface { + constructor( + private sharedVaultServer: SharedVaultServerInterface, + private mutator: MutatorClientInterface, + ) {} + + async execute(localVaults: VaultListingInterface[]): Promise> { + const remoteVaultsResponse = await this.sharedVaultServer.getSharedVaults() + if (isErrorResponse(remoteVaultsResponse)) { + return Result.fail((remoteVaultsResponse.data.error as HttpError).message as string) + } + const remoteVaults = remoteVaultsResponse.data.sharedVaults + + for (const localVault of localVaults) { + if (!localVault.isSharedVaultListing()) { + continue + } + const remoteVault = remoteVaults.find((vault) => vault.uuid === localVault.sharing.sharedVaultUuid) + if (remoteVault) { + await this.mutator.changeItem(localVault, (mutator) => { + /* istanbul ignore next */ + mutator.sharing = { + sharedVaultUuid: remoteVault.uuid, + ownerUserUuid: remoteVault.user_uuid, + fileBytesUsed: remoteVault.file_upload_bytes_used, + } + }) + } + } + + return Result.ok() + } +} diff --git a/packages/services/src/Domain/Strings/Messages.ts b/packages/services/src/Domain/Strings/Messages.ts index 0814b9535..ab3414756 100644 --- a/packages/services/src/Domain/Strings/Messages.ts +++ b/packages/services/src/Domain/Strings/Messages.ts @@ -1,4 +1,4 @@ -import { ProtocolVersion } from '@standardnotes/common' +import { ProtocolVersion } from '@standardnotes/models' export const API_MESSAGE_GENERIC_INVALID_LOGIN = 'A server error occurred while trying to sign in. Please try again.' export const API_MESSAGE_GENERIC_REGISTRATION_FAIL = diff --git a/packages/services/src/Domain/VaultInvite/UseCase/InviteToVault.ts b/packages/services/src/Domain/VaultInvite/UseCase/InviteToVault.ts index 01b55d9f2..bd333ca09 100644 --- a/packages/services/src/Domain/VaultInvite/UseCase/InviteToVault.ts +++ b/packages/services/src/Domain/VaultInvite/UseCase/InviteToVault.ts @@ -123,6 +123,7 @@ export class InviteToVault implements UseCaseInterface(TYPES.ItemManager)) }) + this.factory.set(TYPES.SyncLocalVaultsWithRemoteSharedVaults, () => { + return new SyncLocalVaultsWithRemoteSharedVaults( + this.get(TYPES.SharedVaultServer), + this.get(TYPES.MutatorService), + ) + }) + this.factory.set(TYPES.ChangeAndSaveItem, () => { return new ChangeAndSaveItem( this.get(TYPES.ItemManager), @@ -893,6 +901,7 @@ export class Dependencies { return new SharedVaultService( this.get(TYPES.ItemManager), this.get(TYPES.SessionManager), + this.get(TYPES.SyncLocalVaultsWithRemoteSharedVaults), this.get(TYPES.GetVault), this.get(TYPES.GetOwnedSharedVaults), this.get(TYPES.CreateSharedVault), diff --git a/packages/snjs/lib/Application/Dependencies/Types.ts b/packages/snjs/lib/Application/Dependencies/Types.ts index 5db17ce70..e145da9c9 100644 --- a/packages/snjs/lib/Application/Dependencies/Types.ts +++ b/packages/snjs/lib/Application/Dependencies/Types.ts @@ -98,6 +98,7 @@ export const TYPES = { ValidateItemSigner: Symbol.for('ValidateItemSigner'), GetVault: Symbol.for('GetVault'), GetVaults: Symbol.for('GetVaults'), + SyncLocalVaultsWithRemoteSharedVaults: Symbol.for('SyncLocalVaultsWithRemoteSharedVaults'), GetSharedVaults: Symbol.for('GetSharedVaults'), GetOwnedSharedVaults: Symbol.for('GetOwnedSharedVaults'), ChangeVaultKeyOptions: Symbol.for('ChangeVaultKeyOptions'), diff --git a/packages/snjs/lib/ApplicationGroup/ApplicationDescriptor.ts b/packages/snjs/lib/ApplicationGroup/ApplicationDescriptor.ts index a55b1c8a3..c44c8ae31 100644 --- a/packages/snjs/lib/ApplicationGroup/ApplicationDescriptor.ts +++ b/packages/snjs/lib/ApplicationGroup/ApplicationDescriptor.ts @@ -1,4 +1,4 @@ -import { ApplicationIdentifier } from '@standardnotes/common' +import { ApplicationIdentifier } from '@standardnotes/models' export type ApplicationDescriptor = { identifier: ApplicationIdentifier diff --git a/packages/snjs/lib/Domain/UseCase/GetRevision/GetRevision.spec.ts b/packages/snjs/lib/Domain/UseCase/GetRevision/GetRevision.spec.ts index d43d74da2..6ecf3a1fa 100644 --- a/packages/snjs/lib/Domain/UseCase/GetRevision/GetRevision.spec.ts +++ b/packages/snjs/lib/Domain/UseCase/GetRevision/GetRevision.spec.ts @@ -5,10 +5,10 @@ jest.mock('@standardnotes/models', () => { return { ...original, - isRemotePayloadAllowed: jest.fn(), + checkRemotePayloadAllowed: jest.fn(), } }) -const isRemotePayloadAllowed = require('@standardnotes/models').isRemotePayloadAllowed +const checkRemotePayloadAllowed = require('@standardnotes/models').checkRemotePayloadAllowed import { Revision } from '../../Revision/Revision' @@ -44,7 +44,7 @@ describe('GetRevision', () => { encryptedPayload.copy = jest.fn().mockReturnValue(encryptedPayload) encryptionService.decryptSplitSingle = jest.fn().mockReturnValue(encryptedPayload) - isRemotePayloadAllowed.mockImplementation(() => true) + checkRemotePayloadAllowed.mockImplementation(() => ({ allowed: {} })) }) it('should get revision', async () => { @@ -145,7 +145,7 @@ describe('GetRevision', () => { }) it('should fail if remote payload is not allowed', async () => { - isRemotePayloadAllowed.mockImplementation(() => false) + checkRemotePayloadAllowed.mockImplementation(() => ({ disallowed: {} })) const useCase = createUseCase() diff --git a/packages/snjs/lib/Domain/UseCase/GetRevision/GetRevision.ts b/packages/snjs/lib/Domain/UseCase/GetRevision/GetRevision.ts index 18fee0deb..bdadba5c5 100644 --- a/packages/snjs/lib/Domain/UseCase/GetRevision/GetRevision.ts +++ b/packages/snjs/lib/Domain/UseCase/GetRevision/GetRevision.ts @@ -5,7 +5,7 @@ import { EncryptedPayload, HistoryEntry, isErrorDecryptingPayload, - isRemotePayloadAllowed, + checkRemotePayloadAllowed, NoteContent, PayloadTimestampDefaults, } from '@standardnotes/models' @@ -71,7 +71,8 @@ export class GetRevision implements UseCaseInterface { uuid: sourceItemUuid || revision.item_uuid, }) - if (!isRemotePayloadAllowed(payload as ServerItemResponse)) { + const remotePayloadAllowedResult = checkRemotePayloadAllowed(payload as ServerItemResponse) + if (remotePayloadAllowedResult.disallowed !== undefined) { return Result.fail(`Remote payload is disallowed: ${JSON.stringify(payload)}`) } diff --git a/packages/snjs/lib/Migrations/MigrationServices.ts b/packages/snjs/lib/Migrations/MigrationServices.ts index bc08c8feb..ca465a816 100644 --- a/packages/snjs/lib/Migrations/MigrationServices.ts +++ b/packages/snjs/lib/Migrations/MigrationServices.ts @@ -1,5 +1,5 @@ import { BackupServiceInterface } from '@standardnotes/files' -import { Environment, Platform } from '@standardnotes/models' +import { Environment, Platform, ApplicationIdentifier } from '@standardnotes/models' import { DeviceInterface, InternalEventBusInterface, @@ -8,7 +8,6 @@ import { PreferenceServiceInterface, } from '@standardnotes/services' import { SessionManager } from '../Services/Session/SessionManager' -import { ApplicationIdentifier } from '@standardnotes/common' import { ItemManager } from '@Lib/Services/Items/ItemManager' import { ChallengeService, SingletonManager, FeaturesService, DiskStorageService } from '@Lib/Services' import { LegacySession, MapperInterface } from '@standardnotes/domain-core' diff --git a/packages/snjs/lib/Migrations/StorageReaders/Functions.ts b/packages/snjs/lib/Migrations/StorageReaders/Functions.ts index 62b4228ba..6d6a621e6 100644 --- a/packages/snjs/lib/Migrations/StorageReaders/Functions.ts +++ b/packages/snjs/lib/Migrations/StorageReaders/Functions.ts @@ -1,5 +1,4 @@ -import { ApplicationIdentifier } from '@standardnotes/common' -import { Environment } from '@standardnotes/models' +import { Environment, ApplicationIdentifier } from '@standardnotes/models' import { compareSemVersions, isRightVersionGreaterThanLeft } from '@Lib/Version' import { DeviceInterface } from '@standardnotes/services' import { StorageReader } from './Reader' diff --git a/packages/snjs/lib/Migrations/StorageReaders/Reader.ts b/packages/snjs/lib/Migrations/StorageReaders/Reader.ts index 8ec650acc..9b8f0c4bb 100644 --- a/packages/snjs/lib/Migrations/StorageReaders/Reader.ts +++ b/packages/snjs/lib/Migrations/StorageReaders/Reader.ts @@ -1,5 +1,4 @@ -import { Environment } from '@standardnotes/models' -import { ApplicationIdentifier } from '@standardnotes/common' +import { Environment, ApplicationIdentifier } from '@standardnotes/models' import { DeviceInterface } from '@standardnotes/services' /** diff --git a/packages/snjs/lib/Services/Sync/Account/Response.ts b/packages/snjs/lib/Services/Sync/Account/Response.ts index dc9720cfd..4b0e86c0f 100644 --- a/packages/snjs/lib/Services/Sync/Account/Response.ts +++ b/packages/snjs/lib/Services/Sync/Account/Response.ts @@ -10,6 +10,8 @@ import { NotificationServerHash, AsymmetricMessageServerHash, getErrorFromErrorResponse, + ConflictType, + ServerItemResponse, } from '@standardnotes/responses' import { FilterDisallowedRemotePayloadsAndMap, @@ -44,15 +46,21 @@ export class ServerSyncResponse { const legacyConflicts = this.successResponseData?.unsaved || [] this.rawConflictObjects = conflicts.concat(legacyConflicts) - this.savedPayloads = FilterDisallowedRemotePayloadsAndMap(this.successResponseData?.saved_items || []).map( - (rawItem) => { - return CreateServerSyncSavedPayload(rawItem) - }, + const disallowedPayloads = [] + + const savedItemsFilteringResult = FilterDisallowedRemotePayloadsAndMap(this.successResponseData?.saved_items || []) + this.savedPayloads = savedItemsFilteringResult.filtered.map((rawItem) => { + return CreateServerSyncSavedPayload(rawItem) + }) + disallowedPayloads.push(...savedItemsFilteringResult.disallowed) + + const retrievedItemsFilteringResult = FilterDisallowedRemotePayloadsAndMap( + this.successResponseData?.retrieved_items || [], ) + this.retrievedPayloads = retrievedItemsFilteringResult.filtered + disallowedPayloads.push(...retrievedItemsFilteringResult.disallowed) - this.retrievedPayloads = FilterDisallowedRemotePayloadsAndMap(this.successResponseData?.retrieved_items || []) - - this.conflicts = this.filterConflicts() + this.conflicts = this.filterConflictsAndDisallowedPayloads(disallowedPayloads) this.vaults = this.successResponseData?.shared_vaults || [] @@ -65,20 +73,48 @@ export class ServerSyncResponse { deepFreeze(this) } - private filterConflicts(): TrustedServerConflictMap { + private filterConflictsAndDisallowedPayloads(disallowedPayloads: ServerItemResponse[]): TrustedServerConflictMap { const conflicts = this.rawConflictObjects const trustedConflicts: TrustedServerConflictMap = {} + trustedConflicts[ConflictType.InvalidServerItem] = [] + const invalidServerConflictsArray = trustedConflicts[ConflictType.InvalidServerItem] + + for (const payload of disallowedPayloads) { + invalidServerConflictsArray.push({ + type: ConflictType.InvalidServerItem, + server_item: payload, + }) + } + for (const conflict of conflicts) { let serverItem: FilteredServerItem | undefined let unsavedItem: FilteredServerItem | undefined if (conflict.unsaved_item) { - unsavedItem = FilterDisallowedRemotePayloadsAndMap([conflict.unsaved_item])[0] + const unsavedItemFilteringResult = FilterDisallowedRemotePayloadsAndMap([conflict.unsaved_item]) + if (unsavedItemFilteringResult.filtered.length === 1) { + unsavedItem = unsavedItemFilteringResult.filtered[0] + } + if (unsavedItemFilteringResult.disallowed.length === 1) { + invalidServerConflictsArray.push({ + type: ConflictType.InvalidServerItem, + unsaved_item: unsavedItemFilteringResult.disallowed[0], + }) + } } if (conflict.server_item) { - serverItem = FilterDisallowedRemotePayloadsAndMap([conflict.server_item])[0] + const serverItemFilteringResult = FilterDisallowedRemotePayloadsAndMap([conflict.server_item]) + if (serverItemFilteringResult.filtered.length === 1) { + serverItem = serverItemFilteringResult.filtered[0] + } + if (serverItemFilteringResult.disallowed.length === 1) { + invalidServerConflictsArray.push({ + type: ConflictType.InvalidServerItem, + server_item: serverItemFilteringResult.disallowed[0], + }) + } } if (!trustedConflicts[conflict.type]) { diff --git a/packages/snjs/lib/Services/Sync/Account/ResponseResolver.ts b/packages/snjs/lib/Services/Sync/Account/ResponseResolver.ts index 646606db3..93d9c1798 100644 --- a/packages/snjs/lib/Services/Sync/Account/ResponseResolver.ts +++ b/packages/snjs/lib/Services/Sync/Account/ResponseResolver.ts @@ -92,6 +92,7 @@ export class ServerSyncResponseResolver { ...this.getConflictsForType(ConflictType.SharedVaultInsufficientPermissionsError), ...this.getConflictsForType(ConflictType.SharedVaultNotMemberError), ...this.getConflictsForType(ConflictType.SharedVaultInvalidState), + ...this.getConflictsForType(ConflictType.InvalidServerItem), ] const delta = new DeltaRemoteRejected(this.baseCollection, conflicts) diff --git a/packages/snjs/lib/Services/Sync/Account/Utilities.ts b/packages/snjs/lib/Services/Sync/Account/Utilities.ts index dc7bd1eb0..4b407820b 100644 --- a/packages/snjs/lib/Services/Sync/Account/Utilities.ts +++ b/packages/snjs/lib/Services/Sync/Account/Utilities.ts @@ -1,3 +1,4 @@ +import { Result } from '@standardnotes/domain-core' import { EncryptedPayloadInterface, DeletedPayloadInterface, @@ -10,22 +11,27 @@ import { export function CreatePayloadFromRawServerItem( rawItem: FilteredServerItem, source: PayloadSource, -): EncryptedPayloadInterface | DeletedPayloadInterface { +): Result { if (rawItem.deleted) { - return new DeletedPayload({ ...rawItem, content: undefined, deleted: true }, source) + return Result.ok(new DeletedPayload({ ...rawItem, content: undefined, deleted: true }, source)) } else if (rawItem.content != undefined) { - return new EncryptedPayload( - { - ...rawItem, - items_key_id: rawItem.items_key_id, - content: rawItem.content, - deleted: false, - errorDecrypting: false, - waitingForKey: false, - }, - source, - ) - } else { - throw Error('Unhandled case in createPayloadFromRawItem') + try { + return Result.ok( + new EncryptedPayload( + { + ...rawItem, + items_key_id: rawItem.items_key_id, + content: rawItem.content, + deleted: false, + errorDecrypting: false, + waitingForKey: false, + }, + source, + ), + ) + } catch (error) { + return Result.fail(JSON.stringify(error)) + } } + return Result.fail('Unhandled case in createPayloadFromRawItem') } diff --git a/packages/snjs/lib/Services/Sync/SyncService.ts b/packages/snjs/lib/Services/Sync/SyncService.ts index bafe76072..fe896389f 100644 --- a/packages/snjs/lib/Services/Sync/SyncService.ts +++ b/packages/snjs/lib/Services/Sync/SyncService.ts @@ -1110,7 +1110,12 @@ export class SyncService items: FilteredServerItem[], source: PayloadSource, ): Promise { - const payloads = items.map((i) => CreatePayloadFromRawServerItem(i, source)) + const payloads = items + .map((i) => { + const result = CreatePayloadFromRawServerItem(i, source) + return result.isFailed() ? undefined : result.getValue() + }) + .filter(isNotUndefined) const { encrypted, deleted } = CreateNonDecryptedPayloadSplit(payloads) @@ -1408,9 +1413,16 @@ export class SyncService return } - const receivedPayloads = FilterDisallowedRemotePayloadsAndMap(rawPayloads).map((rawPayload) => { - return CreatePayloadFromRawServerItem(rawPayload, PayloadSource.RemoteRetrieved) - }) + const rawPayloadsFilteringResult = FilterDisallowedRemotePayloadsAndMap(rawPayloads) + const receivedPayloads = rawPayloadsFilteringResult.filtered + .map((rawPayload) => { + const result = CreatePayloadFromRawServerItem(rawPayload, PayloadSource.RemoteRetrieved) + if (result.isFailed()) { + return undefined + } + return result.getValue() + }) + .filter(isNotUndefined) const payloadSplit = CreateNonDecryptedPayloadSplit(receivedPayloads) diff --git a/packages/snjs/lib/index.ts b/packages/snjs/lib/index.ts index 32d61d28c..e4eb70b8c 100644 --- a/packages/snjs/lib/index.ts +++ b/packages/snjs/lib/index.ts @@ -8,7 +8,7 @@ export * from './Migrations' export * from './Services' export * from './Types' export * from './Version' -export * from '@standardnotes/common' +export { KeyParamsOrigination } from '@standardnotes/common' export * from '@standardnotes/domain-core' export * from '@standardnotes/api' export * from '@standardnotes/encryption' diff --git a/packages/snjs/mocha/lib/factory.js b/packages/snjs/mocha/lib/factory.js index 055604104..5d4e919fd 100644 --- a/packages/snjs/mocha/lib/factory.js +++ b/packages/snjs/mocha/lib/factory.js @@ -1,4 +1,3 @@ - import FakeWebCrypto from './fake_web_crypto.js' import { AppContext } from './AppContext.js' import { VaultsContext } from './VaultsContext.js' @@ -303,8 +302,10 @@ export function tomorrow() { return new Date(new Date().setDate(new Date().getDate() + 1)) } -export async function sleep(seconds, reason) { - console.log('[Factory] Sleeping for reason', reason) +export async function sleep(seconds, reason, dontLog = false) { + if (!dontLog) { + console.log('[Factory] Sleeping for reason', reason) + } return Utils.sleep(seconds) } diff --git a/packages/snjs/mocha/payload.test.js b/packages/snjs/mocha/payload.test.js index 97195b849..1bf69315f 100644 --- a/packages/snjs/mocha/payload.test.js +++ b/packages/snjs/mocha/payload.test.js @@ -74,7 +74,7 @@ describe('payload', () => { content_type: ContentType.TYPES.Note, content: '000:somebase64string', }), - 'Unrecognized protocol version 000', + 'EncryptedPayload constructor versionResult is failed', ) }) diff --git a/packages/snjs/mocha/sync_tests/online.test.js b/packages/snjs/mocha/sync_tests/online.test.js index 2dd6c639e..df1cb80b0 100644 --- a/packages/snjs/mocha/sync_tests/online.test.js +++ b/packages/snjs/mocha/sync_tests/online.test.js @@ -113,7 +113,7 @@ describe('online syncing', function () { for (let i = 0; i < syncCount; i++) { application.sync.sync(syncOptions) - await Factory.sleep(0.01) + await Factory.sleep(0.01, undefined, true) } await promise expect(promise).to.be.fulfilled @@ -958,6 +958,11 @@ describe('online syncing', function () { }, }) + context.anticipateConsoleError( + 'Error decrypting payload', + 'The encrypted payload above is not a valid encrypted payload.', + ) + await application.sync.handleSuccessServerResponse({ payloadsSavedOrSaving: [], options: {} }, response) expect(application.payloads.findOne(invalidPayload.uuid)).to.not.be.ok diff --git a/packages/snjs/package.json b/packages/snjs/package.json index 63d7a65b6..3e81cacf6 100644 --- a/packages/snjs/package.json +++ b/packages/snjs/package.json @@ -37,7 +37,7 @@ "@babel/preset-env": "*", "@standardnotes/api": "workspace:*", "@standardnotes/common": "^1.50.0", - "@standardnotes/domain-core": "^1.24.0", + "@standardnotes/domain-core": "^1.25.0", "@standardnotes/domain-events": "^2.108.1", "@standardnotes/encryption": "workspace:*", "@standardnotes/features": "workspace:*", diff --git a/packages/ui-services/package.json b/packages/ui-services/package.json index e3c0b0714..8036d1bf9 100644 --- a/packages/ui-services/package.json +++ b/packages/ui-services/package.json @@ -16,7 +16,7 @@ }, "dependencies": { "@standardnotes/common": "^1.50.0", - "@standardnotes/domain-core": "^1.24.0", + "@standardnotes/domain-core": "^1.25.0", "@standardnotes/features": "workspace:^", "@standardnotes/filepicker": "workspace:^", "@standardnotes/models": "workspace:^", diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults.tsx index b93e19c2d..c7e884430 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults.tsx @@ -77,6 +77,14 @@ const Vaults = () => { }) }, [application.sharedVaults, updateAllData]) + useEffect(() => { + return application.sharedVaults.addEventObserver((event) => { + if (event === SharedVaultServiceEvent.SharedVaultFileStorageUsageChanged) { + void updateAllData() + } + }) + }, [application.sharedVaults, updateAllData]) + useEffect(() => { return application.vaultUsers.addEventObserver((event) => { if (event === VaultUserServiceEvent.UsersChanged) { diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults/VaultItem.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults/VaultItem.tsx index 6fee6bd50..cea705aca 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults/VaultItem.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults/VaultItem.tsx @@ -1,12 +1,13 @@ +import { formatSizeToReadableString } from '@standardnotes/filepicker' +import { ButtonType, VaultListingInterface, isClientDisplayableError } from '@standardnotes/snjs' +import { useCallback, useState } from 'react' + import { useApplication } from '@/Components/ApplicationProvider' import Button from '@/Components/Button/Button' import Icon from '@/Components/Icon/Icon' import ModalOverlay from '@/Components/Modal/ModalOverlay' -import { ButtonType, VaultListingInterface, isClientDisplayableError } from '@standardnotes/snjs' -import { useCallback, useState } from 'react' import ContactInviteModal from '../Invites/ContactInviteModal' import EditVaultModal from './VaultModal/EditVaultModal' - type Props = { vault: VaultListingInterface } @@ -123,6 +124,10 @@ const VaultItem = ({ vault }: Props) => { )} Vault ID: {vault.systemIdentifier} + + File storage used: {formatSizeToReadableString(vault.sharing?.fileBytesUsed ?? 0)} + +