internal: incomplete vault systems behind feature flag (#2340)

This commit is contained in:
Mo
2023-06-30 09:01:56 -05:00
committed by GitHub
parent d16e401bb9
commit b032eb9c9b
638 changed files with 20321 additions and 4813 deletions

View File

@@ -6,4 +6,9 @@ export interface ContextPayload<C extends ItemContent = ItemContent> {
content_type: ContentType
content: C | string | undefined
deleted: boolean
user_uuid?: string
key_system_identifier?: string | undefined
shared_vault_uuid?: string | undefined
last_edited_by_uuid?: string
}

View File

@@ -5,7 +5,7 @@ export interface FilteredServerItem extends ServerItemResponse {
__passed_filter__: true
}
export function CreateFilteredServerItem(item: ServerItemResponse): FilteredServerItem {
function CreateFilteredServerItem(item: ServerItemResponse): FilteredServerItem {
return {
...item,
__passed_filter__: true,

View File

@@ -19,6 +19,8 @@ export function CreateEncryptedBackupFileContextPayload(
updated_at_timestamp: fromPayload.updated_at_timestamp,
updated_at: fromPayload.updated_at,
uuid: fromPayload.uuid,
key_system_identifier: fromPayload.key_system_identifier,
shared_vault_uuid: fromPayload.shared_vault_uuid,
}
}
@@ -35,5 +37,7 @@ export function CreateDecryptedBackupFileContextPayload(
updated_at_timestamp: fromPayload.updated_at_timestamp,
updated_at: fromPayload.updated_at,
uuid: fromPayload.uuid,
key_system_identifier: fromPayload.key_system_identifier,
shared_vault_uuid: fromPayload.shared_vault_uuid,
}
}

View File

@@ -3,6 +3,7 @@ import { ItemContent } from '../Content/ItemContent'
import { DecryptedPayloadInterface, DeletedPayloadInterface, EncryptedPayloadInterface } from '../Payload'
import { useBoolean } from '@standardnotes/utils'
import { EncryptedTransferPayload, isEncryptedTransferPayload } from '../TransferPayload'
import { PersistentSignatureData } from '../../Runtime/Encryption/PersistentSignatureData'
export function isEncryptedLocalStoragePayload(
p: LocalStorageEncryptedContextualPayload | LocalStorageDecryptedContextualPayload,
@@ -25,6 +26,7 @@ export interface LocalStorageEncryptedContextualPayload extends ContextPayload {
updated_at_timestamp: number
updated_at: Date
waitingForKey: boolean
signatureData?: PersistentSignatureData
}
export interface LocalStorageDecryptedContextualPayload<C extends ItemContent = ItemContent> extends ContextPayload {
@@ -36,6 +38,7 @@ export interface LocalStorageDecryptedContextualPayload<C extends ItemContent =
duplicate_of?: string
updated_at_timestamp: number
updated_at: Date
signatureData?: PersistentSignatureData
}
export interface LocalStorageDeletedContextualPayload extends ContextPayload {
@@ -47,6 +50,7 @@ export interface LocalStorageDeletedContextualPayload extends ContextPayload {
duplicate_of?: string
updated_at_timestamp: number
updated_at: Date
signatureData?: PersistentSignatureData
}
export function CreateEncryptedLocalStorageContextPayload(
@@ -68,6 +72,11 @@ export function CreateEncryptedLocalStorageContextPayload(
updated_at: fromPayload.updated_at,
uuid: fromPayload.uuid,
waitingForKey: fromPayload.waitingForKey,
user_uuid: fromPayload.user_uuid,
key_system_identifier: fromPayload.key_system_identifier,
shared_vault_uuid: fromPayload.shared_vault_uuid,
last_edited_by_uuid: fromPayload.last_edited_by_uuid,
signatureData: fromPayload.signatureData,
}
}
@@ -85,6 +94,11 @@ export function CreateDecryptedLocalStorageContextPayload(
updated_at: fromPayload.updated_at,
uuid: fromPayload.uuid,
dirty: useBoolean(fromPayload.dirty, false),
user_uuid: fromPayload.user_uuid,
key_system_identifier: fromPayload.key_system_identifier,
shared_vault_uuid: fromPayload.shared_vault_uuid,
last_edited_by_uuid: fromPayload.last_edited_by_uuid,
signatureData: fromPayload.signatureData,
}
}
@@ -102,5 +116,10 @@ export function CreateDeletedLocalStorageContextPayload(
updated_at_timestamp: fromPayload.updated_at_timestamp,
updated_at: fromPayload.updated_at,
uuid: fromPayload.uuid,
user_uuid: fromPayload.user_uuid,
key_system_identifier: fromPayload.key_system_identifier,
shared_vault_uuid: fromPayload.shared_vault_uuid,
last_edited_by_uuid: fromPayload.last_edited_by_uuid,
signatureData: fromPayload.signatureData,
}
}

View File

@@ -29,6 +29,8 @@ export function CreateEncryptedServerSyncPushPayload(
enc_item_key: fromPayload.enc_item_key,
items_key_id: fromPayload.items_key_id,
auth_hash: fromPayload.auth_hash,
key_system_identifier: fromPayload.key_system_identifier,
shared_vault_uuid: fromPayload.shared_vault_uuid,
}
}
@@ -45,5 +47,7 @@ export function CreateDeletedServerSyncPushPayload(
updated_at: fromPayload.updated_at,
uuid: fromPayload.uuid,
content: undefined,
key_system_identifier: fromPayload.key_system_identifier,
shared_vault_uuid: fromPayload.shared_vault_uuid,
}
}

View File

@@ -16,6 +16,11 @@ export interface ServerSyncSavedContextualPayload {
updated_at_timestamp: number
updated_at: Date
uuid: string
key_system_identifier: string | undefined
shared_vault_uuid: string | undefined
user_uuid: string
duplicate_of?: string
last_edited_by_uuid?: string
}
export function CreateServerSyncSavedPayload(from: FilteredServerItem): ServerSyncSavedContextualPayload {
@@ -27,5 +32,10 @@ export function CreateServerSyncSavedPayload(from: FilteredServerItem): ServerSy
updated_at_timestamp: from.updated_at_timestamp,
updated_at: from.updated_at,
uuid: from.uuid,
key_system_identifier: from.key_system_identifier,
shared_vault_uuid: from.shared_vault_uuid,
user_uuid: from.user_uuid,
duplicate_of: from.duplicate_of,
last_edited_by_uuid: from.last_edited_by_uuid,
}
}

View File

@@ -0,0 +1,4 @@
import { ConflictParams } from '@standardnotes/responses'
import { FilteredServerItem } from './FilteredServerItem'
export type TrustedConflictParams = ConflictParams<FilteredServerItem>

View File

@@ -50,7 +50,7 @@ export class DecryptedItem<C extends ItemContent = ItemContent>
return this.payload.content.references || []
}
public isReferencingItem(item: DecryptedItemInterface): boolean {
public isReferencingItem(item: { uuid: string }): boolean {
return this.references.find((r) => r.uuid === item.uuid) != undefined
}

View File

@@ -10,6 +10,7 @@ import { SingletonStrategy } from '../Types/SingletonStrategy'
import { PayloadInterface } from '../../Payload/Interfaces/PayloadInterface'
import { HistoryEntryInterface } from '../../../Runtime/History/HistoryEntryInterface'
import { isDecryptedItem, isDeletedItem, isEncryptedErroredItem } from '../Interfaces/TypeCheck'
import { PersistentSignatureData } from '../../../Runtime/Encryption/PersistentSignatureData'
export abstract class GenericItem<P extends PayloadInterface = PayloadInterface> implements ItemInterface<P> {
payload: P
@@ -43,6 +44,26 @@ export abstract class GenericItem<P extends PayloadInterface = PayloadInterface>
return this.payload.created_at
}
get key_system_identifier(): string | undefined {
return this.payload.key_system_identifier
}
get user_uuid(): string | undefined {
return this.payload.user_uuid
}
get shared_vault_uuid(): string | undefined {
return this.payload.shared_vault_uuid
}
get last_edited_by_uuid(): string | undefined {
return this.payload.last_edited_by_uuid
}
get signatureData(): PersistentSignatureData | undefined {
return this.payload.signatureData
}
/**
* The date timestamp the server set for this item upon it being synced
* Undefined if never synced to a remote server.

View File

@@ -33,7 +33,7 @@ export interface DecryptedItemInterface<C extends ItemContent = ItemContent>
payloadRepresentation(override?: Partial<DecryptedTransferPayload<C>>): DecryptedPayloadInterface<C>
isReferencingItem(item: DecryptedItemInterface): boolean
isReferencingItem(item: { uuid: string }): boolean
getDomainData(domain: typeof ComponentDataDomain | typeof DefaultAppDomain): undefined | Record<string, unknown>

View File

@@ -5,6 +5,7 @@ import { PredicateInterface } from '../../../Runtime/Predicate/Interface'
import { HistoryEntryInterface } from '../../../Runtime/History'
import { ConflictStrategy } from '../Types/ConflictStrategy'
import { SingletonStrategy } from '../Types/SingletonStrategy'
import { PersistentSignatureData } from '../../../Runtime/Encryption/PersistentSignatureData'
export interface ItemInterface<P extends PayloadInterface = PayloadInterface> {
payload: P
@@ -14,6 +15,11 @@ export interface ItemInterface<P extends PayloadInterface = PayloadInterface> {
readonly updatedAtString?: string
uuid: string
get key_system_identifier(): string | undefined
get user_uuid(): string | undefined
get shared_vault_uuid(): string | undefined
get last_edited_by_uuid(): string | undefined
get signatureData(): PersistentSignatureData | undefined
content_type: ContentType
created_at: Date

View File

@@ -10,13 +10,13 @@ import { DecryptedPayloadInterface } from '../../Payload/Interfaces/DecryptedPay
import { ItemInterface } from '../Interfaces/ItemInterface'
import { getIncrementedDirtyIndex } from '../../../Runtime/DirtyCounter/DirtyCounter'
export class DecryptedItemMutator<C extends ItemContent = ItemContent> extends ItemMutator<
DecryptedPayloadInterface<C>,
DecryptedItemInterface<C>
> {
export class DecryptedItemMutator<
C extends ItemContent = ItemContent,
I extends DecryptedItemInterface<C> = DecryptedItemInterface<C>,
> extends ItemMutator<DecryptedPayloadInterface<C>, I> {
protected mutableContent: C
constructor(item: DecryptedItemInterface<C>, type: MutationType) {
constructor(item: I, type: MutationType) {
super(item, type)
const mutableCopy = Copy(this.immutablePayload.content)
@@ -43,6 +43,8 @@ export class DecryptedItemMutator<C extends ItemContent = ItemContent> extends I
content: this.mutableContent,
dirty: true,
dirtyIndex: getIncrementedDirtyIndex(),
signatureData: undefined,
last_edited_by_uuid: undefined,
})
return result

View File

@@ -3,6 +3,8 @@ import { PayloadInterface } from '../../Payload'
import { ItemInterface } from '../Interfaces/ItemInterface'
import { TransferPayload } from '../../TransferPayload'
import { getIncrementedDirtyIndex } from '../../../Runtime/DirtyCounter/DirtyCounter'
import { KeySystemIdentifier } from '../../../Syncable/KeySystemRootKey/KeySystemIdentifier'
import { ContentTypeUsesRootKeyEncryption } from '../../../Runtime/Encryption/ContentTypeUsesRootKeyEncryption'
/**
* An item mutator takes in an item, and an operation, and returns the resulting payload.
@@ -51,6 +53,26 @@ export class ItemMutator<
})
}
public set key_system_identifier(keySystemIdentifier: KeySystemIdentifier | undefined) {
if (ContentTypeUsesRootKeyEncryption(this.immutableItem.content_type)) {
throw new Error('Cannot set key_system_identifier on a root key encrypted item')
}
this.immutablePayload = this.immutablePayload.copy({
key_system_identifier: keySystemIdentifier,
})
}
public set shared_vault_uuid(sharedVaultUuid: string | undefined) {
if (ContentTypeUsesRootKeyEncryption(this.immutableItem.content_type)) {
throw new Error('Cannot set shared_vault_uuid on a root key encrypted item')
}
this.immutablePayload = this.immutablePayload.copy({
shared_vault_uuid: sharedVaultUuid,
})
}
public set errorDecrypting(_: boolean) {
throw Error('This method is no longer implemented')
}

View File

@@ -5,6 +5,8 @@ import { PayloadSource } from '../Types/PayloadSource'
import { TransferPayload } from '../../TransferPayload/Interfaces/TransferPayload'
import { ItemContent } from '../../Content/ItemContent'
import { SyncResolvedParams, SyncResolvedPayload } from '../../../Runtime/Deltas/Utilities/SyncResolvedPayload'
import { PersistentSignatureData } from '../../../Runtime/Encryption/PersistentSignatureData'
import { ContentTypeUsesRootKeyEncryption } from '../../../Runtime/Encryption/ContentTypeUsesRootKeyEncryption'
type RequiredKeepUndefined<T> = { [K in keyof T]-?: [T[K]] } extends infer U
? U extends Record<keyof U, [unknown]>
@@ -33,18 +35,28 @@ export abstract class PurePayload<T extends TransferPayload<C>, C extends ItemCo
readonly lastSyncEnd?: Date
readonly duplicate_of?: string
readonly user_uuid?: string
readonly key_system_identifier?: string | undefined
readonly shared_vault_uuid?: string | undefined
readonly last_edited_by_uuid?: string
readonly signatureData?: PersistentSignatureData
constructor(rawPayload: T, source = PayloadSource.Constructor) {
this.source = source
this.uuid = rawPayload.uuid
if (!this.uuid) {
if (!rawPayload.uuid) {
throw Error(
`Attempting to construct payload with null uuid
Content type: ${rawPayload.content_type}`,
)
}
if (rawPayload.key_system_identifier && ContentTypeUsesRootKeyEncryption(rawPayload.content_type)) {
throw new Error('Rootkey-encrypted payload should not have a key system identifier')
}
this.source = source
this.uuid = rawPayload.uuid
this.content = rawPayload.content
this.content_type = rawPayload.content_type
this.deleted = useBoolean(rawPayload.deleted, false)
@@ -63,6 +75,13 @@ export abstract class PurePayload<T extends TransferPayload<C>, C extends ItemCo
this.dirtyIndex = rawPayload.dirtyIndex
this.globalDirtyIndexAtLastSync = rawPayload.globalDirtyIndexAtLastSync
this.user_uuid = rawPayload.user_uuid ?? undefined
this.key_system_identifier = rawPayload.key_system_identifier ?? undefined
this.shared_vault_uuid = rawPayload.shared_vault_uuid ?? undefined
this.last_edited_by_uuid = rawPayload.last_edited_by_uuid ?? undefined
this.signatureData = rawPayload.signatureData
const timeToAllowSubclassesToFinishConstruction = 0
setTimeout(() => {
deepFreeze(this)
@@ -85,6 +104,11 @@ export abstract class PurePayload<T extends TransferPayload<C>, C extends ItemCo
globalDirtyIndexAtLastSync: this.globalDirtyIndexAtLastSync,
lastSyncBegan: this.lastSyncBegan,
lastSyncEnd: this.lastSyncEnd,
key_system_identifier: this.key_system_identifier,
user_uuid: this.user_uuid,
shared_vault_uuid: this.shared_vault_uuid,
last_edited_by_uuid: this.last_edited_by_uuid,
signatureData: this.signatureData,
}
return comprehensive

View File

@@ -3,6 +3,7 @@ import { ContentType } from '@standardnotes/common'
import { ItemContent } from '../../Content/ItemContent'
import { TransferPayload } from '../../TransferPayload/Interfaces/TransferPayload'
import { PayloadSource } from '../Types/PayloadSource'
import { PersistentSignatureData } from '../../../Runtime/Encryption/PersistentSignatureData'
export interface PayloadInterface<T extends TransferPayload = TransferPayload, C extends ItemContent = ItemContent> {
readonly source: PayloadSource
@@ -22,12 +23,18 @@ export interface PayloadInterface<T extends TransferPayload = TransferPayload, C
readonly dirtyIndex?: number
readonly globalDirtyIndexAtLastSync?: number
readonly dirty?: boolean
readonly signatureData?: PersistentSignatureData
readonly lastSyncBegan?: Date
readonly lastSyncEnd?: Date
readonly duplicate_of?: string
readonly user_uuid?: string
readonly key_system_identifier?: string | undefined
readonly shared_vault_uuid?: string | undefined
readonly last_edited_by_uuid?: string
/**
* "Ejected" means a payload for
* generic, non-contextual consumption, such as saving to a backup file or syncing

View File

@@ -0,0 +1,15 @@
import { PayloadInterface } from './../Interfaces/PayloadInterface'
import { VaultListingInterface } from '../../../Syncable/VaultListing/VaultListingInterface'
export function PayloadVaultOverrides(
vault: VaultListingInterface | undefined,
): Pick<PayloadInterface, 'key_system_identifier' | 'shared_vault_uuid'> {
if (!vault) {
return {}
}
return {
key_system_identifier: vault.systemIdentifier,
shared_vault_uuid: vault.isSharedVaultListing() ? vault.sharing.sharedVaultUuid : undefined,
}
}

View File

@@ -5,6 +5,8 @@ export enum PayloadSource {
*/
Constructor = 1,
LocalDatabaseLoaded = 2,
RemoteRetrieved,
RemoteSaved,

View File

@@ -10,4 +10,5 @@ export * from './Interfaces/TypeCheck'
export * from './Interfaces/UnionTypes'
export * from './Types/PayloadSource'
export * from './Types/EmitSource'
export * from './Types/TimestampDefaults'
export * from './Overrides/TimestampDefaults'
export * from './Overrides/VaultOverride'

View File

@@ -1,5 +1,6 @@
import { ContentType } from '@standardnotes/common'
import { ItemContent } from '../../Content/ItemContent'
import { PersistentSignatureData } from '../../../Runtime/Encryption/PersistentSignatureData'
export interface TransferPayload<C extends ItemContent = ItemContent> {
uuid: string
@@ -15,9 +16,16 @@ export interface TransferPayload<C extends ItemContent = ItemContent> {
dirtyIndex?: number
globalDirtyIndexAtLastSync?: number
dirty?: boolean
signatureData?: PersistentSignatureData
lastSyncBegan?: Date
lastSyncEnd?: Date
duplicate_of?: string
user_uuid?: string
key_system_identifier?: string | undefined
shared_vault_uuid?: string | undefined
last_edited_by_uuid?: string
}

View File

@@ -0,0 +1,16 @@
import { ProtocolVersion } from '@standardnotes/common'
import { KeySystemIdentifier } from '../../Syncable/KeySystemRootKey/KeySystemIdentifier'
import { KeySystemRootKeyPasswordType } from './KeySystemRootKeyPasswordType'
/**
* Key params are public data that contain information about how a root key was created.
* Given a keyParams object and a password, clients can compute a root key that was created
* previously.
*/
export interface KeySystemRootKeyParamsInterface {
systemIdentifier: KeySystemIdentifier
seed: string
version: ProtocolVersion
passwordType: KeySystemRootKeyPasswordType
creationTimestamp: number
}

View File

@@ -0,0 +1,4 @@
export enum KeySystemRootKeyPasswordType {
UserInputted = 'user_inputted',
Randomized = 'randomized',
}

View File

@@ -1,5 +1,6 @@
import { ApplicationIdentifier, ProtocolVersion } from '@standardnotes/common'
import { RootKeyContentSpecialized } from './RootKeyContent'
import { PkcKeyPair } from '@standardnotes/sncrypto-common'
export type RawKeychainValue = Record<ApplicationIdentifier, NamespacedRootKeyInKeychain>
@@ -7,6 +8,8 @@ export interface NamespacedRootKeyInKeychain {
version: ProtocolVersion
masterKey: string
dataAuthenticationKey?: string
encryptionKeyPair: PkcKeyPair | undefined
signingKeyPair: PkcKeyPair | undefined
}
export type RootKeyContentInStorage = RootKeyContentSpecialized

View File

@@ -1,3 +1,4 @@
import { PkcKeyPair } from '@standardnotes/sncrypto-common'
import { ItemContent } from '../../Abstract/Content/ItemContent'
import { ProtocolVersion, AnyKeyParamsContent } from '@standardnotes/common'
@@ -7,6 +8,9 @@ export interface RootKeyContentSpecialized {
serverPassword?: string
dataAuthenticationKey?: string
keyParams: AnyKeyParamsContent
encryptionKeyPair?: PkcKeyPair
signingKeyPair?: PkcKeyPair
}
export type RootKeyContent = RootKeyContentSpecialized & ItemContent

View File

@@ -1,3 +1,4 @@
import { PkcKeyPair } from '@standardnotes/sncrypto-common'
import { ProtocolVersion } from '@standardnotes/common'
import { DecryptedItemInterface } from '../../Abstract/Item/Interfaces/DecryptedItem'
import { RootKeyParamsInterface } from '../KeyParams/RootKeyParamsInterface'
@@ -6,11 +7,16 @@ import { RootKeyContent } from './RootKeyContent'
export interface RootKeyInterface extends DecryptedItemInterface<RootKeyContent> {
readonly keyParams: RootKeyParamsInterface
get keyVersion(): ProtocolVersion
get itemsKey(): string
get masterKey(): string
get serverPassword(): string | undefined
get dataAuthenticationKey(): string | undefined
get encryptionKeyPair(): PkcKeyPair | undefined
get signingKeyPair(): PkcKeyPair | undefined
compare(otherKey: RootKeyInterface): boolean
persistableValueWhenWrapping(): RootKeyContentInStorage
getKeychainValue(): NamespacedRootKeyInKeychain

View File

@@ -0,0 +1,7 @@
import { PkcKeyPair } from '@standardnotes/sncrypto-common'
import { RootKeyInterface } from './RootKeyInterface'
export interface RootKeyWithKeyPairsInterface extends RootKeyInterface {
get encryptionKeyPair(): PkcKeyPair
get signingKeyPair(): PkcKeyPair
}

View File

@@ -0,0 +1,3 @@
export type AsymmetricMessageDataCommon = {
recipientUuid: string
}

View File

@@ -0,0 +1,12 @@
import { AsymmetricMessageSenderKeypairChanged } from './MessageTypes/AsymmetricMessageSenderKeypairChanged'
import { AsymmetricMessageSharedVaultInvite } from './MessageTypes/AsymmetricMessageSharedVaultInvite'
import { AsymmetricMessageSharedVaultMetadataChanged } from './MessageTypes/AsymmetricMessageSharedVaultMetadataChanged'
import { AsymmetricMessageSharedVaultRootKeyChanged } from './MessageTypes/AsymmetricMessageSharedVaultRootKeyChanged'
import { AsymmetricMessageTrustedContactShare } from './MessageTypes/AsymmetricMessageTrustedContactShare'
export type AsymmetricMessagePayload =
| AsymmetricMessageSharedVaultRootKeyChanged
| AsymmetricMessageTrustedContactShare
| AsymmetricMessageSenderKeypairChanged
| AsymmetricMessageSharedVaultInvite
| AsymmetricMessageSharedVaultMetadataChanged

View File

@@ -0,0 +1,7 @@
export enum AsymmetricMessagePayloadType {
ContactShare = 'contact-share',
SharedVaultRootKeyChanged = 'shared-vault-root-key-changed',
SenderKeypairChanged = 'sender-keypair-changed',
SharedVaultInvite = 'shared-vault-invite',
SharedVaultMetadataChanged = 'shared-vault-metadata-changed',
}

View File

@@ -0,0 +1,10 @@
import { AsymmetricMessageDataCommon } from '../AsymmetricMessageDataCommon'
import { AsymmetricMessagePayloadType } from '../AsymmetricMessagePayloadType'
export type AsymmetricMessageSenderKeypairChanged = {
type: AsymmetricMessagePayloadType.SenderKeypairChanged
data: AsymmetricMessageDataCommon & {
newEncryptionPublicKey: string
newSigningPublicKey: string
}
}

View File

@@ -0,0 +1,16 @@
import { KeySystemRootKeyContentSpecialized } from '../../../Syncable/KeySystemRootKey/KeySystemRootKeyContent'
import { TrustedContactContentSpecialized } from '../../../Syncable/TrustedContact/TrustedContactContent'
import { AsymmetricMessageDataCommon } from '../AsymmetricMessageDataCommon'
import { AsymmetricMessagePayloadType } from '../AsymmetricMessagePayloadType'
export type AsymmetricMessageSharedVaultInvite = {
type: AsymmetricMessagePayloadType.SharedVaultInvite
data: AsymmetricMessageDataCommon & {
rootKey: KeySystemRootKeyContentSpecialized
trustedContacts: TrustedContactContentSpecialized[]
metadata: {
name: string
description?: string
}
}
}

View File

@@ -0,0 +1,11 @@
import { AsymmetricMessageDataCommon } from '../AsymmetricMessageDataCommon'
import { AsymmetricMessagePayloadType } from '../AsymmetricMessagePayloadType'
export type AsymmetricMessageSharedVaultMetadataChanged = {
type: AsymmetricMessagePayloadType.SharedVaultMetadataChanged
data: AsymmetricMessageDataCommon & {
sharedVaultUuid: string
name: string
description?: string
}
}

View File

@@ -0,0 +1,8 @@
import { KeySystemRootKeyContentSpecialized } from '../../../Syncable/KeySystemRootKey/KeySystemRootKeyContent'
import { AsymmetricMessageDataCommon } from '../AsymmetricMessageDataCommon'
import { AsymmetricMessagePayloadType } from '../AsymmetricMessagePayloadType'
export type AsymmetricMessageSharedVaultRootKeyChanged = {
type: AsymmetricMessagePayloadType.SharedVaultRootKeyChanged
data: AsymmetricMessageDataCommon & { rootKey: KeySystemRootKeyContentSpecialized }
}

View File

@@ -0,0 +1,8 @@
import { TrustedContactContentSpecialized } from '../../../Syncable/TrustedContact/TrustedContactContent'
import { AsymmetricMessageDataCommon } from '../AsymmetricMessageDataCommon'
import { AsymmetricMessagePayloadType } from '../AsymmetricMessagePayloadType'
export type AsymmetricMessageTrustedContactShare = {
type: AsymmetricMessagePayloadType.ContactShare
data: AsymmetricMessageDataCommon & { trustedContact: TrustedContactContentSpecialized }
}

View File

@@ -1,10 +1,10 @@
import { ItemCounter } from './ItemCounter'
import { NoteContent } from '../../../Syncable/Note/NoteContent'
import { ContentType } from '@standardnotes/common'
import { DecryptedItem, EncryptedItem } from '../../../Abstract/Item'
import { DecryptedPayload, EncryptedPayload, PayloadTimestampDefaults } from '../../../Abstract/Payload'
import { ItemCollection } from './ItemCollection'
import { FillItemContent } from '../../../Abstract/Content/ItemContent'
import { TagItemsIndex } from './TagItemsIndex'
import { ItemDelta } from '../../Index/ItemDelta'
import { AnyItemInterface } from '../../../Abstract/Item/Interfaces/UnionTypes'
@@ -48,7 +48,7 @@ describe('tag notes index', () => {
it('should count both notes and files', () => {
const collection = new ItemCollection()
const index = new TagItemsIndex(collection)
const index = new ItemCounter(collection)
const decryptedNote = createDecryptedItem('note')
const decryptedFile = createDecryptedItem('file')
@@ -61,7 +61,7 @@ describe('tag notes index', () => {
it('should decrement count after decrypted note becomes errored', () => {
const collection = new ItemCollection()
const index = new TagItemsIndex(collection)
const index = new ItemCounter(collection)
const decryptedItem = createDecryptedItem()
collection.set(decryptedItem)

View File

@@ -4,25 +4,28 @@ import { isTag, SNTag } from '../../../Syncable/Tag/Tag'
import { SNIndex } from '../../Index/SNIndex'
import { ItemCollection } from './ItemCollection'
import { ItemDelta } from '../../Index/ItemDelta'
import { isDecryptedItem, ItemInterface } from '../../../Abstract/Item'
import { DecryptedItemInterface, isDecryptedItem, ItemInterface } from '../../../Abstract/Item'
import { CriteriaValidatorInterface } from '../../Display/Validator/CriteriaValidatorInterface'
import { CollectionCriteriaValidator } from '../../Display/Validator/CollectionCriteriaValidator'
import { ExcludeVaultsCriteriaValidator } from '../../Display/Validator/ExcludeVaultsCriteriaValidator'
import { ExclusiveVaultCriteriaValidator } from '../../Display/Validator/ExclusiveVaultCriteriaValidator'
import { HiddenContentCriteriaValidator } from '../../Display/Validator/HiddenContentCriteriaValidator'
import { CustomFilterCriteriaValidator } from '../../Display/Validator/CustomFilterCriteriaValidator'
import { AnyDisplayOptions, VaultDisplayOptions } from '../../Display'
import { isExclusioanaryOptionsValue } from '../../Display/VaultDisplayOptionsTypes'
type AllNotesUuidSignifier = undefined
export type TagItemCountChangeObserver = (tagUuid: string | AllNotesUuidSignifier) => void
export class TagItemsIndex implements SNIndex {
export class ItemCounter implements SNIndex {
private tagToItemsMap: Partial<Record<string, Set<string>>> = {}
private allCountableItems = new Set<string>()
private countableItemsByType = new Map<ContentType, Set<string>>()
private displayOptions?: AnyDisplayOptions
private vaultDisplayOptions?: VaultDisplayOptions
constructor(private collection: ItemCollection, public observers: TagItemCountChangeObserver[] = []) {}
private isItemCountable = (item: ItemInterface) => {
if (isDecryptedItem(item)) {
return !item.archived && !item.trashed && !item.conflictOf
}
return false
}
public addCountChangeObserver(observer: TagItemCountChangeObserver): () => void {
this.observers.push(observer)
@@ -32,10 +35,14 @@ export class TagItemsIndex implements SNIndex {
}
}
private notifyObservers(tagUuid: string | undefined) {
for (const observer of this.observers) {
observer(tagUuid)
}
public setDisplayOptions(options: AnyDisplayOptions) {
this.displayOptions = options
this.receiveItemChanges(this.collection.all())
}
public setVaultDisplayOptions(options: VaultDisplayOptions) {
this.vaultDisplayOptions = options
this.receiveItemChanges(this.collection.all())
}
public allCountableItemsCount(): number {
@@ -64,6 +71,47 @@ export class TagItemsIndex implements SNIndex {
this.receiveTagChanges(tags)
}
private passesAllFilters(element: DecryptedItemInterface): boolean {
if (!this.displayOptions) {
return true
}
const filters: CriteriaValidatorInterface[] = [new CollectionCriteriaValidator(this.collection, element)]
if (this.vaultDisplayOptions) {
const options = this.vaultDisplayOptions.getOptions()
if (isExclusioanaryOptionsValue(options)) {
filters.push(new ExcludeVaultsCriteriaValidator([...options.exclude, ...options.locked], element))
} else {
filters.push(new ExclusiveVaultCriteriaValidator(options.exclusive, element))
}
}
if ('hiddenContentTypes' in this.displayOptions && this.displayOptions.hiddenContentTypes) {
filters.push(new HiddenContentCriteriaValidator(this.displayOptions.hiddenContentTypes, element))
}
if ('customFilter' in this.displayOptions && this.displayOptions.customFilter) {
filters.push(new CustomFilterCriteriaValidator(this.displayOptions.customFilter, element))
}
return filters.every((f) => f.passes())
}
private isItemCountable = (item: ItemInterface) => {
if (isDecryptedItem(item)) {
const passesFilters = this.passesAllFilters(item)
return passesFilters && !item.archived && !item.trashed && !item.conflictOf
}
return false
}
private notifyObservers(tagUuid: string | undefined) {
for (const observer of this.observers) {
observer(tagUuid)
}
}
private receiveTagChanges(tags: SNTag[]): void {
for (const tag of tags) {
const uuids = tag.references

View File

@@ -1,15 +1,16 @@
import { ConflictDelta } from './Conflict'
import { PayloadEmitSource } from '../../Abstract/Payload'
import { FullyFormedPayloadInterface, PayloadEmitSource } from '../../Abstract/Payload'
import { ImmutablePayloadCollection } from '../Collection/Payload/ImmutablePayloadCollection'
import { HistoryMap } from '../History'
import { extendSyncDelta, SyncDeltaEmit } from './Abstract/DeltaEmit'
import { SyncDeltaInterface } from './Abstract/SyncDeltaInterface'
import { payloadByFinalizingSyncState } from './Utilities/ApplyDirtyState'
import { ConflictConflictingDataParams } from '@standardnotes/responses'
export class DeltaRemoteDataConflicts implements SyncDeltaInterface {
constructor(
readonly baseCollection: ImmutablePayloadCollection,
readonly applyCollection: ImmutablePayloadCollection,
readonly conflicts: ConflictConflictingDataParams<FullyFormedPayloadInterface>[],
readonly historyMap: HistoryMap,
) {}
@@ -20,18 +21,18 @@ export class DeltaRemoteDataConflicts implements SyncDeltaInterface {
source: PayloadEmitSource.RemoteRetrieved,
}
for (const apply of this.applyCollection.all()) {
const base = this.baseCollection.find(apply.uuid)
for (const conflict of this.conflicts) {
const base = this.baseCollection.find(conflict.server_item.uuid)
const isBaseDeleted = base == undefined
if (isBaseDeleted) {
result.emits.push(payloadByFinalizingSyncState(apply, this.baseCollection))
result.emits.push(payloadByFinalizingSyncState(conflict.server_item, this.baseCollection))
continue
}
const delta = new ConflictDelta(this.baseCollection, base, apply, this.historyMap)
const delta = new ConflictDelta(this.baseCollection, base, conflict.server_item, this.historyMap)
extendSyncDelta(result, delta.result())
}

View File

@@ -1,10 +1,14 @@
import { ContentType } from '@standardnotes/common'
import { FillItemContent } from '../../Abstract/Content/ItemContent'
import { DecryptedPayload, PayloadTimestampDefaults } from '../../Abstract/Payload'
import { DecryptedPayload, FullyFormedPayloadInterface, PayloadTimestampDefaults } from '../../Abstract/Payload'
import { NoteContent } from '../../Syncable/Note'
import { PayloadCollection } from '../Collection/Payload/PayloadCollection'
import { DeltaRemoteRejected } from './RemoteRejected'
import { ImmutablePayloadCollection } from '../Collection/Payload/ImmutablePayloadCollection'
import { ConflictParams, ConflictType } from '@standardnotes/responses'
import { UuidGenerator } from '@standardnotes/utils'
UuidGenerator.SetGenerator(() => String(Math.random()))
describe('remote rejected delta', () => {
it('rejected payloads should not map onto app state', async () => {
@@ -30,10 +34,12 @@ describe('remote rejected delta', () => {
dirty: true,
})
const delta = new DeltaRemoteRejected(
ImmutablePayloadCollection.FromCollection(baseCollection),
ImmutablePayloadCollection.WithPayloads([rejectedPayload]),
)
const entry: ConflictParams<FullyFormedPayloadInterface> = {
type: ConflictType.ContentTypeError,
unsaved_item: rejectedPayload,
} as unknown as ConflictParams<FullyFormedPayloadInterface>
const delta = new DeltaRemoteRejected(ImmutablePayloadCollection.FromCollection(baseCollection), [entry])
const result = delta.result()
const payload = result.emits[0] as DecryptedPayload<NoteContent>

View File

@@ -1,35 +1,48 @@
import { PayloadSource } from '../../Abstract/Payload/Types/PayloadSource'
import { PayloadEmitSource } from '../../Abstract/Payload'
import { DeletedPayload, FullyFormedPayloadInterface, PayloadEmitSource } from '../../Abstract/Payload'
import { ImmutablePayloadCollection } from '../Collection/Payload/ImmutablePayloadCollection'
import { SyncDeltaEmit } from './Abstract/DeltaEmit'
import { SyncDeltaInterface } from './Abstract/SyncDeltaInterface'
import { SyncResolvedPayload } from './Utilities/SyncResolvedPayload'
import { BuildSyncResolvedParams, SyncResolvedPayload } from './Utilities/SyncResolvedPayload'
import {
ConflictParams,
ConflictParamsWithServerItem,
ConflictParamsWithUnsavedItem,
ConflictParamsWithServerAndUnsavedItem,
conflictParamsHasServerItemAndUnsavedItem,
conflictParamsHasOnlyServerItem,
conflictParamsHasOnlyUnsavedItem,
ConflictType,
} from '@standardnotes/responses'
import { PayloadsByDuplicating } from '../../Utilities/Payload/PayloadsByDuplicating'
import { ContentType } from '@standardnotes/common'
export class DeltaRemoteRejected implements SyncDeltaInterface {
constructor(
readonly baseCollection: ImmutablePayloadCollection,
readonly applyCollection: ImmutablePayloadCollection,
readonly conflicts: ConflictParams<FullyFormedPayloadInterface>[],
) {}
public result(): SyncDeltaEmit {
const results: SyncResolvedPayload[] = []
for (const apply of this.applyCollection.all()) {
const base = this.baseCollection.find(apply.uuid)
const vaultErrors: ConflictType[] = [
ConflictType.SharedVaultInsufficientPermissionsError,
ConflictType.SharedVaultNotMemberError,
ConflictType.SharedVaultInvalidState,
ConflictType.SharedVaultSnjsVersionError,
]
if (!base) {
continue
for (const conflict of this.conflicts) {
if (vaultErrors.includes(conflict.type)) {
results.push(...this.handleVaultError(conflict))
} else if (conflictParamsHasServerItemAndUnsavedItem(conflict)) {
results.push(...this.getResultForConflictWithServerItemAndUnsavedItem(conflict))
} else if (conflictParamsHasOnlyServerItem(conflict)) {
results.push(...this.getResultForConflictWithOnlyServerItem(conflict))
} else if (conflictParamsHasOnlyUnsavedItem(conflict)) {
results.push(...this.getResultForConflictWithOnlyUnsavedItem(conflict))
}
const result = base.copyAsSyncResolved(
{
dirty: false,
lastSyncEnd: new Date(),
},
PayloadSource.RemoteSaved,
)
results.push(result)
}
return {
@@ -37,4 +50,177 @@ export class DeltaRemoteRejected implements SyncDeltaInterface {
source: PayloadEmitSource.RemoteSaved,
}
}
private handleVaultError(conflict: ConflictParams<FullyFormedPayloadInterface>): SyncResolvedPayload[] {
const base = this.baseCollection.find(conflict.unsaved_item.uuid)
if (!base) {
return []
}
if (conflict.type === ConflictType.SharedVaultNotMemberError) {
return this.resultByDuplicatingBasePayloadAsNonVaultedAndRemovingBaseItemLocally(base)
}
if (base.content_type === ContentType.KeySystemItemsKey) {
return this.discardChangesOfBasePayload(base)
}
if (conflict.server_item) {
return this.resultByDuplicatingBasePayloadAsNonVaultedAndTakingServerPayloadAsCanonical(
base,
conflict.server_item,
)
} else {
return this.resultByDuplicatingBasePayloadAsNonVaultedAndDiscardingChangesOfOriginal(base)
}
}
private discardChangesOfBasePayload(base: FullyFormedPayloadInterface): SyncResolvedPayload[] {
const undirtiedPayload = base.copyAsSyncResolved(
{
dirty: false,
lastSyncEnd: new Date(),
},
PayloadSource.RemoteSaved,
)
return [undirtiedPayload]
}
private getResultForConflictWithOnlyUnsavedItem(
conflict: ConflictParamsWithUnsavedItem<FullyFormedPayloadInterface>,
): SyncResolvedPayload[] {
const base = this.baseCollection.find(conflict.unsaved_item.uuid)
if (!base) {
return []
}
const result = base.copyAsSyncResolved(
{
dirty: false,
lastSyncEnd: new Date(),
},
PayloadSource.RemoteSaved,
)
return [result]
}
private getResultForConflictWithOnlyServerItem(
conflict: ConflictParamsWithServerItem<FullyFormedPayloadInterface>,
): SyncResolvedPayload[] {
const base = this.baseCollection.find(conflict.server_item.uuid)
if (!base) {
return []
}
return this.resultByDuplicatingBasePayloadIntoNewUuidAndTakingServerPayloadAsCanonical(base, conflict.server_item)
}
private getResultForConflictWithServerItemAndUnsavedItem(
conflict: ConflictParamsWithServerAndUnsavedItem<FullyFormedPayloadInterface>,
): SyncResolvedPayload[] {
const base = this.baseCollection.find(conflict.server_item.uuid)
if (!base) {
return []
}
return this.resultByDuplicatingBasePayloadIntoNewUuidAndTakingServerPayloadAsCanonical(base, conflict.server_item)
}
private resultByDuplicatingBasePayloadIntoNewUuidAndTakingServerPayloadAsCanonical(
basePayload: FullyFormedPayloadInterface,
serverPayload: FullyFormedPayloadInterface,
): SyncResolvedPayload[] {
const duplicateBasePayloadIntoNewUuid = PayloadsByDuplicating({
payload: basePayload,
baseCollection: this.baseCollection,
isConflict: true,
source: serverPayload.source,
})
const takeServerPayloadAsCanonical = serverPayload.copyAsSyncResolved(
{
lastSyncBegan: basePayload.lastSyncBegan,
dirty: false,
lastSyncEnd: new Date(),
},
serverPayload.source,
)
return duplicateBasePayloadIntoNewUuid.concat([takeServerPayloadAsCanonical])
}
private resultByDuplicatingBasePayloadAsNonVaultedAndTakingServerPayloadAsCanonical(
basePayload: FullyFormedPayloadInterface,
serverPayload: FullyFormedPayloadInterface,
): SyncResolvedPayload[] {
const duplicateBasePayloadIntoNewUuid = PayloadsByDuplicating({
payload: basePayload.copy({
key_system_identifier: undefined,
shared_vault_uuid: undefined,
}),
baseCollection: this.baseCollection,
isConflict: true,
source: serverPayload.source,
})
const takeServerPayloadAsCanonical = serverPayload.copyAsSyncResolved(
{
lastSyncBegan: basePayload.lastSyncBegan,
dirty: false,
lastSyncEnd: new Date(),
},
serverPayload.source,
)
return duplicateBasePayloadIntoNewUuid.concat([takeServerPayloadAsCanonical])
}
private resultByDuplicatingBasePayloadAsNonVaultedAndDiscardingChangesOfOriginal(
basePayload: FullyFormedPayloadInterface,
): SyncResolvedPayload[] {
const duplicateBasePayloadWithoutVault = PayloadsByDuplicating({
payload: basePayload.copy({
key_system_identifier: undefined,
shared_vault_uuid: undefined,
}),
baseCollection: this.baseCollection,
isConflict: true,
source: basePayload.source,
})
return [...duplicateBasePayloadWithoutVault, ...this.discardChangesOfBasePayload(basePayload)]
}
private resultByDuplicatingBasePayloadAsNonVaultedAndRemovingBaseItemLocally(
basePayload: FullyFormedPayloadInterface,
): SyncResolvedPayload[] {
const duplicateBasePayloadWithoutVault = PayloadsByDuplicating({
payload: basePayload.copy({
key_system_identifier: undefined,
shared_vault_uuid: undefined,
}),
baseCollection: this.baseCollection,
isConflict: true,
source: basePayload.source,
})
const locallyDeletedBasePayload = new DeletedPayload(
{
...basePayload,
content: undefined,
deleted: true,
key_system_identifier: undefined,
shared_vault_uuid: undefined,
...BuildSyncResolvedParams({
dirty: false,
lastSyncEnd: new Date(),
}),
},
PayloadSource.RemoteSaved,
)
return [...duplicateBasePayloadWithoutVault, locallyDeletedBasePayload as SyncResolvedPayload]
}
}

View File

@@ -36,7 +36,7 @@ export class DeltaRemoteRetrieved implements SyncDeltaInterface {
* or if the item is locally dirty, filter it out of retrieved_items, and add to potential conflicts.
*/
for (const apply of this.applyCollection.all()) {
if (apply.content_type === ContentType.ItemsKey) {
if (apply.content_type === ContentType.ItemsKey || apply.content_type === ContentType.KeySystemItemsKey) {
const itemsKeyDeltaEmit = new ItemsKeyDelta(this.baseCollection, [apply]).result()
extendSyncDelta(result, itemsKeyDeltaEmit)

View File

@@ -1,4 +1,4 @@
import { ServerSyncSavedContextualPayload } from './../../Abstract/Contextual/ServerSyncSaved'
import { ServerSyncSavedContextualPayload } from '../../Abstract/Contextual/ServerSyncSaved'
import { DeletedPayload } from './../../Abstract/Payload/Implementations/DeletedPayload'
import { ImmutablePayloadCollection } from '../Collection/Payload/ImmutablePayloadCollection'
import { PayloadSource } from '../../Abstract/Payload/Types/PayloadSource'

View File

@@ -2,10 +2,11 @@ import { extendArray, filterFromArray, Uuids } from '@standardnotes/utils'
import { ImmutablePayloadCollection } from '../Collection/Payload/ImmutablePayloadCollection'
import { PayloadsByAlternatingUuid } from '../../Utilities/Payload/PayloadsByAlternatingUuid'
import { isDecryptedPayload } from '../../Abstract/Payload/Interfaces/TypeCheck'
import { PayloadEmitSource } from '../../Abstract/Payload'
import { FullyFormedPayloadInterface, PayloadEmitSource } from '../../Abstract/Payload'
import { SyncDeltaEmit } from './Abstract/DeltaEmit'
import { SyncDeltaInterface } from './Abstract/SyncDeltaInterface'
import { SyncResolvedPayload } from './Utilities/SyncResolvedPayload'
import { ConflictUuidConflictParams } from '@standardnotes/responses'
/**
* UUID conflicts can occur if a user attmpts to import an old data
@@ -15,22 +16,22 @@ import { SyncResolvedPayload } from './Utilities/SyncResolvedPayload'
export class DeltaRemoteUuidConflicts implements SyncDeltaInterface {
constructor(
readonly baseCollection: ImmutablePayloadCollection,
readonly applyCollection: ImmutablePayloadCollection,
readonly conflicts: ConflictUuidConflictParams<FullyFormedPayloadInterface>[],
) {}
public result(): SyncDeltaEmit {
const results: SyncResolvedPayload[] = []
const baseCollectionCopy = this.baseCollection.mutableCopy()
for (const apply of this.applyCollection.all()) {
for (const conflict of this.conflicts) {
/**
* The payload in question may have been modified as part of alternating a uuid for
* another item. For example, alternating a uuid for a note will also affect the
* referencing tag, which would be added to `results`, but could also be inside
* of this.applyCollection. In this case we'd prefer the most recently modified value.
*/
const moreRecent = results.find((r) => r.uuid === apply.uuid)
const useApply = moreRecent || apply
const moreRecent = results.find((r) => r.uuid === conflict.unsaved_item.uuid)
const useApply = moreRecent || conflict.unsaved_item
if (!isDecryptedPayload(useApply)) {
continue

View File

@@ -1,8 +1,8 @@
import { createNoteWithContent } from '../../Utilities/Test/SpecUtils'
import { ItemCollection } from '../Collection/Item/ItemCollection'
import { SNNote } from '../../Syncable/Note/Note'
import { itemsMatchingOptions } from './Search/SearchUtilities'
import { FilterDisplayOptions } from './DisplayOptions'
import { notesAndFilesMatchingOptions } from './Search/SearchUtilities'
import { NotesAndFilesDisplayOptions } from './DisplayOptions'
describe('item display options', () => {
const collectionWithNotes = function (titles: (string | undefined)[] = [], bodies: string[] = []) {
@@ -23,31 +23,31 @@ describe('item display options', () => {
it('string query title', () => {
const query = 'foo'
const options: FilterDisplayOptions = {
const options: NotesAndFilesDisplayOptions = {
searchQuery: { query: query, includeProtectedNoteText: true },
}
} as jest.Mocked<NotesAndFilesDisplayOptions>
const collection = collectionWithNotes(['hello', 'fobar', 'foobar', 'foo'])
expect(itemsMatchingOptions(options, collection.all() as SNNote[], collection)).toHaveLength(2)
expect(notesAndFilesMatchingOptions(options, collection.all() as SNNote[], collection)).toHaveLength(2)
})
it('string query text', async function () {
const query = 'foo'
const options: FilterDisplayOptions = {
const options: NotesAndFilesDisplayOptions = {
searchQuery: { query: query, includeProtectedNoteText: true },
}
} as jest.Mocked<NotesAndFilesDisplayOptions>
const collection = collectionWithNotes(
[undefined, undefined, undefined, undefined],
['hello', 'fobar', 'foobar', 'foo'],
)
expect(itemsMatchingOptions(options, collection.all() as SNNote[], collection)).toHaveLength(2)
expect(notesAndFilesMatchingOptions(options, collection.all() as SNNote[], collection)).toHaveLength(2)
})
it('string query title and text', async function () {
const query = 'foo'
const options: FilterDisplayOptions = {
const options: NotesAndFilesDisplayOptions = {
searchQuery: { query: query, includeProtectedNoteText: true },
}
} as jest.Mocked<NotesAndFilesDisplayOptions>
const collection = collectionWithNotes(['hello', 'foobar'], ['foo', 'fobar'])
expect(itemsMatchingOptions(options, collection.all() as SNNote[], collection)).toHaveLength(2)
expect(notesAndFilesMatchingOptions(options, collection.all() as SNNote[], collection)).toHaveLength(2)
})
})

View File

@@ -5,21 +5,28 @@ import { CollectionSortDirection, CollectionSortProperty } from '../Collection/C
import { SearchQuery } from './Search/Types'
import { DisplayControllerCustomFilter } from './Types'
export type DisplayOptions = FilterDisplayOptions & DisplayControllerOptions
export interface FilterDisplayOptions {
tags?: SNTag[]
views?: SmartView[]
searchQuery?: SearchQuery
export interface GenericDisplayOptions {
includePinned?: boolean
includeProtected?: boolean
includeTrashed?: boolean
includeArchived?: boolean
}
export interface DisplayControllerOptions {
sortBy: CollectionSortProperty
sortDirection: CollectionSortDirection
export interface NotesAndFilesDisplayOptions extends GenericDisplayOptions {
tags?: SNTag[]
views?: SmartView[]
searchQuery?: SearchQuery
hiddenContentTypes?: ContentType[]
customFilter?: DisplayControllerCustomFilter
}
export type TagsDisplayOptions = GenericDisplayOptions
export interface DisplayControllerDisplayOptions extends GenericDisplayOptions {
sortBy: CollectionSortProperty
sortDirection: CollectionSortDirection
}
export type NotesAndFilesDisplayControllerOptions = NotesAndFilesDisplayOptions & DisplayControllerDisplayOptions
export type TagsDisplayControllerOptions = TagsDisplayOptions & DisplayControllerDisplayOptions
export type AnyDisplayOptions = NotesAndFilesDisplayOptions | TagsDisplayOptions | GenericDisplayOptions

View File

@@ -5,11 +5,11 @@ import { CompoundPredicate } from '../Predicate/CompoundPredicate'
import { ItemWithTags } from './Search/ItemWithTags'
import { itemMatchesQuery, itemPassesFilters } from './Search/SearchUtilities'
import { ItemFilter, ReferenceLookupCollection, SearchableDecryptedItem } from './Search/Types'
import { FilterDisplayOptions } from './DisplayOptions'
import { NotesAndFilesDisplayOptions } from './DisplayOptions'
import { SystemViewId } from '../../Syncable/SmartView'
export function computeUnifiedFilterForDisplayOptions(
options: FilterDisplayOptions,
options: NotesAndFilesDisplayOptions,
collection: ReferenceLookupCollection,
additionalFilters: ItemFilter[] = [],
): ItemFilter {
@@ -21,7 +21,7 @@ export function computeUnifiedFilterForDisplayOptions(
}
export function computeFiltersForDisplayOptions(
options: FilterDisplayOptions,
options: NotesAndFilesDisplayOptions,
collection: ReferenceLookupCollection,
): ItemFilter[] {
const filters: ItemFilter[] = []

View File

@@ -2,11 +2,19 @@ import { ContentType } from '@standardnotes/common'
import { compareValues } from '@standardnotes/utils'
import { isDeletedItem, isEncryptedItem } from '../../Abstract/Item'
import { ItemDelta } from '../Index/ItemDelta'
import { DisplayControllerOptions } from './DisplayOptions'
import { AnyDisplayOptions, DisplayControllerDisplayOptions, GenericDisplayOptions } from './DisplayOptions'
import { sortTwoItems } from './SortTwoItems'
import { UuidToSortedPositionMap, DisplayItem, ReadonlyItemCollection } from './Types'
import { CriteriaValidatorInterface } from './Validator/CriteriaValidatorInterface'
import { CollectionCriteriaValidator } from './Validator/CollectionCriteriaValidator'
import { CustomFilterCriteriaValidator } from './Validator/CustomFilterCriteriaValidator'
import { ExcludeVaultsCriteriaValidator } from './Validator/ExcludeVaultsCriteriaValidator'
import { ExclusiveVaultCriteriaValidator } from './Validator/ExclusiveVaultCriteriaValidator'
import { HiddenContentCriteriaValidator } from './Validator/HiddenContentCriteriaValidator'
import { VaultDisplayOptions } from './VaultDisplayOptions'
import { isExclusioanaryOptionsValue } from './VaultDisplayOptionsTypes'
export class ItemDisplayController<I extends DisplayItem> {
export class ItemDisplayController<I extends DisplayItem, O extends AnyDisplayOptions = GenericDisplayOptions> {
private sortMap: UuidToSortedPositionMap = {}
private sortedItems: I[] = []
private needsSort = true
@@ -14,7 +22,8 @@ export class ItemDisplayController<I extends DisplayItem> {
constructor(
private readonly collection: ReadonlyItemCollection,
public readonly contentTypes: ContentType[],
private options: DisplayControllerOptions,
private options: DisplayControllerDisplayOptions & O,
private vaultOptions?: VaultDisplayOptions,
) {
this.filterThenSortElements(this.collection.all(this.contentTypes) as I[])
}
@@ -23,7 +32,18 @@ export class ItemDisplayController<I extends DisplayItem> {
return this.sortedItems
}
setDisplayOptions(displayOptions: Partial<DisplayControllerOptions>): void {
public getDisplayOptions(): DisplayControllerDisplayOptions & O {
return this.options
}
setVaultDisplayOptions(vaultOptions?: VaultDisplayOptions): void {
this.vaultOptions = vaultOptions
this.needsSort = true
this.filterThenSortElements(this.collection.all(this.contentTypes) as I[])
}
setDisplayOptions(displayOptions: Partial<DisplayControllerDisplayOptions & O>): void {
this.options = { ...this.options, ...displayOptions }
this.needsSort = true
@@ -37,6 +57,29 @@ export class ItemDisplayController<I extends DisplayItem> {
this.filterThenSortElements(items as I[])
}
private passesAllFilters(element: I): boolean {
const filters: CriteriaValidatorInterface[] = [new CollectionCriteriaValidator(this.collection, element)]
if (this.vaultOptions) {
const options = this.vaultOptions.getOptions()
if (isExclusioanaryOptionsValue(options)) {
filters.push(new ExcludeVaultsCriteriaValidator([...options.exclude, ...options.locked], element))
} else {
filters.push(new ExclusiveVaultCriteriaValidator(options.exclusive, element))
}
}
if ('hiddenContentTypes' in this.options && this.options.hiddenContentTypes) {
filters.push(new HiddenContentCriteriaValidator(this.options.hiddenContentTypes, element))
}
if ('customFilter' in this.options && this.options.customFilter) {
filters.push(new CustomFilterCriteriaValidator(this.options.customFilter, element))
}
return filters.every((f) => f.passes())
}
private filterThenSortElements(elements: I[]): void {
for (const element of elements) {
const previousIndex = this.sortMap[element.uuid]
@@ -61,13 +104,7 @@ export class ItemDisplayController<I extends DisplayItem> {
continue
}
const passes = !this.collection.has(element.uuid)
? false
: this.options.hiddenContentTypes?.includes(element.content_type)
? false
: this.options.customFilter
? this.options.customFilter(element)
: true
const passes = this.passesAllFilters(element)
if (passes) {
if (previousElement != undefined) {

View File

@@ -1,6 +1,6 @@
import { ContentType } from '@standardnotes/common'
import { SNTag } from '../../../Syncable/Tag'
import { FilterDisplayOptions } from '../DisplayOptions'
import { NotesAndFilesDisplayOptions } from '../DisplayOptions'
import { computeFiltersForDisplayOptions } from '../DisplayOptionsToFilters'
import { SearchableItem } from './SearchableItem'
import { ReferenceLookupCollection, ItemFilter, SearchQuery, SearchableDecryptedItem } from './Types'
@@ -13,8 +13,8 @@ enum MatchResult {
Uuid = 5,
}
export function itemsMatchingOptions(
options: FilterDisplayOptions,
export function notesAndFilesMatchingOptions(
options: NotesAndFilesDisplayOptions,
fromItems: SearchableDecryptedItem[],
collection: ReferenceLookupCollection,
): SearchableItem[] {

View File

@@ -0,0 +1,11 @@
import { ItemInterface } from '../../../Abstract/Item'
import { ReadonlyItemCollection } from '../Types'
import { CriteriaValidatorInterface } from './CriteriaValidatorInterface'
export class CollectionCriteriaValidator implements CriteriaValidatorInterface {
constructor(private collection: ReadonlyItemCollection, private element: ItemInterface) {}
public passes(): boolean {
return this.collection.has(this.element.uuid)
}
}

View File

@@ -0,0 +1,3 @@
export interface CriteriaValidatorInterface {
passes(): boolean
}

View File

@@ -0,0 +1,11 @@
import { DecryptedItemInterface } from '../../../Abstract/Item'
import { DisplayControllerCustomFilter } from '../Types'
import { CriteriaValidatorInterface } from './CriteriaValidatorInterface'
export class CustomFilterCriteriaValidator implements CriteriaValidatorInterface {
constructor(private customFilter: DisplayControllerCustomFilter, private element: DecryptedItemInterface) {}
public passes(): boolean {
return this.customFilter(this.element)
}
}

View File

@@ -0,0 +1,15 @@
import { CriteriaValidatorInterface } from './CriteriaValidatorInterface'
import { DecryptedItemInterface } from '../../../Abstract/Item'
import { KeySystemIdentifier } from '../../../Syncable/KeySystemRootKey/KeySystemIdentifier'
export class ExcludeVaultsCriteriaValidator implements CriteriaValidatorInterface {
constructor(private excludeVaults: KeySystemIdentifier[], private element: DecryptedItemInterface) {}
public passes(): boolean {
const doesElementBelongToExcludedVault = this.excludeVaults.some(
(vault) => this.element.key_system_identifier === vault,
)
return !doesElementBelongToExcludedVault
}
}

View File

@@ -0,0 +1,11 @@
import { CriteriaValidatorInterface } from './CriteriaValidatorInterface'
import { DecryptedItemInterface } from '../../../Abstract/Item'
import { KeySystemIdentifier } from '../../../Syncable/KeySystemRootKey/KeySystemIdentifier'
export class ExclusiveVaultCriteriaValidator implements CriteriaValidatorInterface {
constructor(private exclusiveVault: KeySystemIdentifier, private element: DecryptedItemInterface) {}
public passes(): boolean {
return this.element.key_system_identifier === this.exclusiveVault
}
}

View File

@@ -0,0 +1,11 @@
import { DecryptedItemInterface } from './../../../Abstract/Item/Interfaces/DecryptedItem'
import { ContentType } from '@standardnotes/common'
import { CriteriaValidatorInterface } from './CriteriaValidatorInterface'
export class HiddenContentCriteriaValidator implements CriteriaValidatorInterface {
constructor(private hiddenContentTypes: ContentType[], private element: DecryptedItemInterface) {}
public passes(): boolean {
return !this.hiddenContentTypes.includes(this.element.content_type)
}
}

View File

@@ -0,0 +1,109 @@
import { VaultListingInterface } from '../../Syncable/VaultListing/VaultListingInterface'
import { uniqueArray } from '@standardnotes/utils'
import {
ExclusioanaryOptions,
ExclusiveOptions,
VaultDisplayOptionsPersistable,
isExclusioanaryOptionsValue,
} from './VaultDisplayOptionsTypes'
import { KeySystemIdentifier } from '../../Syncable/KeySystemRootKey/KeySystemIdentifier'
function KeySystemIdentifiers(vaults: VaultListingInterface[]): KeySystemIdentifier[] {
return vaults.map((vault) => vault.systemIdentifier)
}
export class VaultDisplayOptions {
constructor(private readonly options: ExclusioanaryOptions | ExclusiveOptions) {}
public getOptions(): ExclusioanaryOptions | ExclusiveOptions {
return this.options
}
public getExclusivelyShownVault(): KeySystemIdentifier {
if (isExclusioanaryOptionsValue(this.options)) {
throw new Error('Not in exclusive display mode')
}
return this.options.exclusive
}
public isInExclusiveDisplayMode(): boolean {
return !isExclusioanaryOptionsValue(this.options)
}
public isVaultExplicitelyExcluded(vault: VaultListingInterface): boolean {
if (isExclusioanaryOptionsValue(this.options)) {
return this.options.exclude.some((excludedVault) => excludedVault === vault.systemIdentifier)
} else if (this.options.exclusive) {
return this.options.exclusive !== vault.systemIdentifier
}
throw new Error('Invalid vault display options')
}
isVaultExclusivelyShown(vault: VaultListingInterface): boolean {
return !isExclusioanaryOptionsValue(this.options) && this.options.exclusive === vault.systemIdentifier
}
isVaultDisabledOrLocked(vault: VaultListingInterface): boolean {
if (isExclusioanaryOptionsValue(this.options)) {
const matchingLocked = this.options.locked.find((lockedVault) => lockedVault === vault.systemIdentifier)
if (matchingLocked) {
return true
}
}
return this.isVaultExplicitelyExcluded(vault)
}
getPersistableValue(): VaultDisplayOptionsPersistable {
return this.options
}
newOptionsByIntakingLockedVaults(lockedVaults: VaultListingInterface[]): VaultDisplayOptions {
if (isExclusioanaryOptionsValue(this.options)) {
return new VaultDisplayOptions({ exclude: this.options.exclude, locked: KeySystemIdentifiers(lockedVaults) })
} else {
return new VaultDisplayOptions({ exclusive: this.options.exclusive })
}
}
newOptionsByExcludingVault(vault: VaultListingInterface, lockedVaults: VaultListingInterface[]): VaultDisplayOptions {
return this.newOptionsByExcludingVaults([vault], lockedVaults)
}
newOptionsByExcludingVaults(
vaults: VaultListingInterface[],
lockedVaults: VaultListingInterface[],
): VaultDisplayOptions {
if (isExclusioanaryOptionsValue(this.options)) {
return new VaultDisplayOptions({
exclude: uniqueArray([...this.options.exclude, ...KeySystemIdentifiers(vaults)]),
locked: KeySystemIdentifiers(lockedVaults),
})
} else {
return new VaultDisplayOptions({
exclude: KeySystemIdentifiers(vaults),
locked: KeySystemIdentifiers(lockedVaults),
})
}
}
newOptionsByUnexcludingVault(
vault: VaultListingInterface,
lockedVaults: VaultListingInterface[],
): VaultDisplayOptions {
if (isExclusioanaryOptionsValue(this.options)) {
return new VaultDisplayOptions({
exclude: this.options.exclude.filter((excludedVault) => excludedVault !== vault.systemIdentifier),
locked: KeySystemIdentifiers(lockedVaults),
})
} else {
return new VaultDisplayOptions({ exclude: [], locked: KeySystemIdentifiers(lockedVaults) })
}
}
static FromPersistableValue(value: VaultDisplayOptionsPersistable): VaultDisplayOptions {
return new VaultDisplayOptions(value)
}
}

View File

@@ -0,0 +1,12 @@
import { KeySystemIdentifier } from '../../Syncable/KeySystemRootKey/KeySystemIdentifier'
export type ExclusioanaryOptions = { exclude: KeySystemIdentifier[]; locked: KeySystemIdentifier[] }
export type ExclusiveOptions = { exclusive: KeySystemIdentifier }
export function isExclusioanaryOptionsValue(
options: ExclusioanaryOptions | ExclusiveOptions,
): options is ExclusioanaryOptions {
return 'exclude' in options || 'locked' in options
}
export type VaultDisplayOptionsPersistable = ExclusioanaryOptions | ExclusiveOptions

View File

@@ -6,3 +6,5 @@ export * from './Search/SearchableItem'
export * from './Search/SearchUtilities'
export * from './Search/Types'
export * from './Types'
export * from './VaultDisplayOptions'
export * from './VaultDisplayOptionsTypes'

View File

@@ -0,0 +1,5 @@
import { ContentType } from '@standardnotes/common'
export function ContentTypeUsesKeySystemRootKeyEncryption(contentType: ContentType): boolean {
return contentType === ContentType.KeySystemItemsKey
}

View File

@@ -0,0 +1,6 @@
import { ContentType } from '@standardnotes/common'
import { ContentTypesUsingRootKeyEncryption } from './ContentTypesUsingRootKeyEncryption'
export function ContentTypeUsesRootKeyEncryption(contentType: ContentType): boolean {
return ContentTypesUsingRootKeyEncryption().includes(contentType)
}

View File

@@ -0,0 +1,11 @@
import { ContentType } from '@standardnotes/common'
export function ContentTypesUsingRootKeyEncryption(): ContentType[] {
return [
ContentType.RootKey,
ContentType.ItemsKey,
ContentType.EncryptedStorage,
ContentType.TrustedContact,
ContentType.KeySystemRootKey,
]
}

View File

@@ -0,0 +1,19 @@
export type PersistentSignatureData =
| {
required: true
contentHash: string
result: {
passes: boolean
publicKey: string
signature: string
}
}
| {
required: false
contentHash: string
result?: {
passes: boolean
publicKey: string
signature: string
}
}

View File

@@ -0,0 +1,11 @@
import { ProtocolVersion } from '@standardnotes/common'
import { ItemContent, SpecializedContent } from '../../Abstract/Content/ItemContent'
export interface KeySystemItemsKeyContentSpecialized extends SpecializedContent {
version: ProtocolVersion
creationTimestamp: number
itemsKey: string
rootKeyToken: string
}
export type KeySystemItemsKeyContent = KeySystemItemsKeyContentSpecialized & ItemContent

View File

@@ -0,0 +1,11 @@
import { ProtocolVersion } from '@standardnotes/common'
import { DecryptedItemInterface } from '../../Abstract/Item/Interfaces/DecryptedItem'
import { KeySystemItemsKeyContent } from './KeySystemItemsKeyContent'
export interface KeySystemItemsKeyInterface extends DecryptedItemInterface<KeySystemItemsKeyContent> {
readonly creationTimestamp: number
readonly rootKeyToken: string
get keyVersion(): ProtocolVersion
get itemsKey(): string
}

View File

@@ -0,0 +1,4 @@
import { DecryptedItemMutator } from '../../Abstract/Item/Mutator/DecryptedItemMutator'
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface KeySystemItemsKeyMutatorInterface extends DecryptedItemMutator {}

View File

@@ -0,0 +1 @@
export type KeySystemIdentifier = string

View File

@@ -0,0 +1,54 @@
import { ContentType, ProtocolVersion } from '@standardnotes/common'
import { ConflictStrategy, DecryptedItem } from '../../Abstract/Item'
import { DecryptedPayloadInterface } from '../../Abstract/Payload'
import { HistoryEntryInterface } from '../../Runtime/History'
import { KeySystemRootKeyContent } from './KeySystemRootKeyContent'
import { KeySystemRootKeyInterface } from './KeySystemRootKeyInterface'
import { KeySystemIdentifier } from './KeySystemIdentifier'
import { KeySystemRootKeyParamsInterface } from '../../Local/KeyParams/KeySystemRootKeyParamsInterface'
export function isKeySystemRootKey(x: { content_type: ContentType }): x is KeySystemRootKey {
return x.content_type === ContentType.KeySystemRootKey
}
export class KeySystemRootKey extends DecryptedItem<KeySystemRootKeyContent> implements KeySystemRootKeyInterface {
keyParams: KeySystemRootKeyParamsInterface
systemIdentifier: KeySystemIdentifier
key: string
keyVersion: ProtocolVersion
token: string
constructor(payload: DecryptedPayloadInterface<KeySystemRootKeyContent>) {
super(payload)
this.keyParams = payload.content.keyParams
this.systemIdentifier = payload.content.systemIdentifier
this.key = payload.content.key
this.keyVersion = payload.content.keyVersion
this.token = payload.content.token
}
override strategyWhenConflictingWithItem(
item: KeySystemRootKey,
_previousRevision?: HistoryEntryInterface,
): ConflictStrategy {
const baseKeyTimestamp = this.keyParams.creationTimestamp
const incomingKeyTimestamp = item.keyParams.creationTimestamp
return incomingKeyTimestamp > baseKeyTimestamp ? ConflictStrategy.KeepApply : ConflictStrategy.KeepBase
}
get itemsKey(): string {
return this.key
}
override get key_system_identifier(): undefined {
return undefined
}
override get shared_vault_uuid(): undefined {
return undefined
}
}

View File

@@ -0,0 +1,16 @@
import { ProtocolVersion } from '@standardnotes/common'
import { ItemContent } from '../../Abstract/Content/ItemContent'
import { KeySystemIdentifier } from './KeySystemIdentifier'
import { KeySystemRootKeyParamsInterface } from '../../Local/KeyParams/KeySystemRootKeyParamsInterface'
export type KeySystemRootKeyContentSpecialized = {
keyParams: KeySystemRootKeyParamsInterface
systemIdentifier: KeySystemIdentifier
key: string
keyVersion: ProtocolVersion
token: string
}
export type KeySystemRootKeyContent = KeySystemRootKeyContentSpecialized & ItemContent

View File

@@ -0,0 +1,38 @@
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'
export interface KeySystemRootKeyInterface extends DecryptedItemInterface<KeySystemRootKeyContent> {
keyParams: KeySystemRootKeyParamsInterface
systemIdentifier: KeySystemIdentifier
key: string
keyVersion: ProtocolVersion
/**
* A token is passed to all items keys created while this root key was active.
* When determining which items key a client should use to encrypt new items or new changes,
* it should look for items keys which have the current root key token. This prevents
* the server from dictating which items key a client should use, and also prevents a server from withholding
* items keys from sync results, which would otherwise compel a client to choose between its available items keys,
* which may be old or rotated.
*
* This token is part of the encrypted payload of both the root key and corresponding items keys. While not
* necessarily destructive if leaked, it prevents a malicious server from creating a compromised items key for a vault.
*/
token: string
get itemsKey(): string
/**
* Key system root keys pertain to a key system, but they are not actually encrypted inside a key system, but rather
* saved as a normal item in the user's account. An item's key_system_identifier tells the cryptographic system which
* keys to use to encrypt, but a key system rootkey's systemIdentifier is just a reference to that identifier that doesn't
* bind the item to a specific vault system's cryptographic keys.
*/
get key_system_identifier(): undefined
get shared_vault_uuid(): undefined
}

View File

@@ -0,0 +1,4 @@
import { DecryptedItemMutator } from '../../Abstract/Item'
import { KeySystemRootKeyContent } from './KeySystemRootKeyContent'
export class KeySystemRootKeyMutator extends DecryptedItemMutator<KeySystemRootKeyContent> {}

View File

@@ -0,0 +1,5 @@
export enum KeySystemRootKeyStorageMode {
Synced = 'synced',
Local = 'local',
Ephemeral = 'ephemeral',
}

View File

@@ -9,10 +9,10 @@ import { FillItemContent } from '../../Abstract/Content/ItemContent'
import { Predicate } from '../../Runtime/Predicate/Predicate'
import { CompoundPredicate } from '../../Runtime/Predicate/CompoundPredicate'
import { PayloadTimestampDefaults } from '../../Abstract/Payload'
import { FilterDisplayOptions } from '../../Runtime/Display'
import { NotesAndFilesDisplayOptions } from '../../Runtime/Display'
import { FileItem } from '../File'
export function BuildSmartViews(options: FilterDisplayOptions): SmartView[] {
export function BuildSmartViews(options: NotesAndFilesDisplayOptions): SmartView[] {
const notes = new SmartView(
new DecryptedPayload({
uuid: SystemViewId.AllNotes,
@@ -100,7 +100,7 @@ export function BuildSmartViews(options: FilterDisplayOptions): SmartView[] {
return [notes, files, starred, archived, trash, untagged, conflicts]
}
function allNotesPredicate(options: FilterDisplayOptions) {
function allNotesPredicate(options: NotesAndFilesDisplayOptions) {
const subPredicates: Predicate<SNNote>[] = [new Predicate('content_type', '=', ContentType.Note)]
if (options.includeTrashed === false) {
@@ -120,7 +120,7 @@ function allNotesPredicate(options: FilterDisplayOptions) {
return predicate
}
function filesPredicate(options: FilterDisplayOptions) {
function filesPredicate(options: NotesAndFilesDisplayOptions) {
const subPredicates: Predicate<FileItem>[] = [new Predicate('content_type', '=', ContentType.File)]
if (options.includeTrashed === false) {
@@ -140,7 +140,7 @@ function filesPredicate(options: FilterDisplayOptions) {
return predicate
}
function archivedNotesPredicate(options: FilterDisplayOptions) {
function archivedNotesPredicate(options: NotesAndFilesDisplayOptions) {
const subPredicates: Predicate<SNNote>[] = [
new Predicate('archived', '=', true),
new Predicate('content_type', '=', ContentType.Note),
@@ -159,7 +159,7 @@ function archivedNotesPredicate(options: FilterDisplayOptions) {
return predicate
}
function trashedNotesPredicate(options: FilterDisplayOptions) {
function trashedNotesPredicate(options: NotesAndFilesDisplayOptions) {
const subPredicates: Predicate<SNNote>[] = [
new Predicate('trashed', '=', true),
new Predicate('content_type', '=', ContentType.Note),
@@ -178,7 +178,7 @@ function trashedNotesPredicate(options: FilterDisplayOptions) {
return predicate
}
function untaggedNotesPredicate(options: FilterDisplayOptions) {
function untaggedNotesPredicate(options: NotesAndFilesDisplayOptions) {
const subPredicates = [
new Predicate('content_type', '=', ContentType.Note),
new Predicate<ItemWithTags>('tagsCount', '=', 0),
@@ -197,7 +197,7 @@ function untaggedNotesPredicate(options: FilterDisplayOptions) {
return predicate
}
function starredNotesPredicate(options: FilterDisplayOptions) {
function starredNotesPredicate(options: NotesAndFilesDisplayOptions) {
const subPredicates: Predicate<SNNote>[] = [
new Predicate('starred', '=', true),
new Predicate('content_type', '=', ContentType.Note),
@@ -216,7 +216,7 @@ function starredNotesPredicate(options: FilterDisplayOptions) {
return predicate
}
function conflictsPredicate(options: FilterDisplayOptions) {
function conflictsPredicate(options: NotesAndFilesDisplayOptions) {
const subPredicates: Predicate<SNNote>[] = [new Predicate('content_type', '=', ContentType.Note)]
if (options.includeTrashed === false) {

View File

@@ -0,0 +1,73 @@
import { ContactPublicKeySetInterface } from './ContactPublicKeySetInterface'
import { ContactPublicKeySetJsonInterface } from './ContactPublicKeySetJsonInterface'
export class ContactPublicKeySet implements ContactPublicKeySetInterface {
encryption: string
signing: string
timestamp: Date
isRevoked: boolean
previousKeySet?: ContactPublicKeySet
constructor(
encryption: string,
signing: string,
timestamp: Date,
isRevoked: boolean,
previousKeySet: ContactPublicKeySet | undefined,
) {
this.encryption = encryption
this.signing = signing
this.timestamp = timestamp
this.isRevoked = isRevoked
this.previousKeySet = previousKeySet
}
public findKeySet(params: {
targetEncryptionPublicKey: string
targetSigningPublicKey: string
}): ContactPublicKeySetInterface | undefined {
if (this.encryption === params.targetEncryptionPublicKey && this.signing === params.targetSigningPublicKey) {
return this
}
if (this.previousKeySet) {
return this.previousKeySet.findKeySet(params)
}
return undefined
}
public findKeySetWithSigningKey(signingKey: string): ContactPublicKeySetInterface | undefined {
if (this.signing === signingKey) {
return this
}
if (this.previousKeySet) {
return this.previousKeySet.findKeySetWithSigningKey(signingKey)
}
return undefined
}
findKeySetWithPublicKey(publicKey: string): ContactPublicKeySetInterface | undefined {
if (this.encryption === publicKey) {
return this
}
if (this.previousKeySet) {
return this.previousKeySet.findKeySetWithPublicKey(publicKey)
}
return undefined
}
static FromJson(json: ContactPublicKeySetJsonInterface): ContactPublicKeySetInterface {
return new ContactPublicKeySet(
json.encryption,
json.signing,
new Date(json.timestamp),
json.isRevoked,
json.previousKeySet ? ContactPublicKeySet.FromJson(json.previousKeySet) : undefined,
)
}
}

View File

@@ -0,0 +1,15 @@
export interface ContactPublicKeySetInterface {
encryption: string
signing: string
timestamp: Date
isRevoked: boolean
previousKeySet?: ContactPublicKeySetInterface
findKeySet(params: {
targetEncryptionPublicKey: string
targetSigningPublicKey: string
}): ContactPublicKeySetInterface | undefined
findKeySetWithPublicKey(publicKey: string): ContactPublicKeySetInterface | undefined
findKeySetWithSigningKey(signingKey: string): ContactPublicKeySetInterface | undefined
}

View File

@@ -0,0 +1,7 @@
export interface ContactPublicKeySetJsonInterface {
encryption: string
signing: string
timestamp: Date
isRevoked: boolean
previousKeySet?: ContactPublicKeySetJsonInterface
}

View File

@@ -0,0 +1,8 @@
import { ContactPublicKeySetInterface } from './ContactPublicKeySetInterface'
export type FindPublicKeySetResult =
| {
publicKeySet: ContactPublicKeySetInterface
current: boolean
}
| undefined

View File

@@ -0,0 +1,77 @@
import { ConflictStrategy, DecryptedItem, DecryptedItemInterface } from '../../Abstract/Item'
import { DecryptedPayloadInterface } from '../../Abstract/Payload'
import { HistoryEntryInterface } from '../../Runtime/History'
import { TrustedContactContent } from './TrustedContactContent'
import { TrustedContactInterface } from './TrustedContactInterface'
import { FindPublicKeySetResult } from './PublicKeySet/FindPublicKeySetResult'
import { ContactPublicKeySet } from './PublicKeySet/ContactPublicKeySet'
import { ContactPublicKeySetInterface } from './PublicKeySet/ContactPublicKeySetInterface'
import { Predicate } from '../../Runtime/Predicate/Predicate'
export class TrustedContact extends DecryptedItem<TrustedContactContent> implements TrustedContactInterface {
static singletonPredicate = new Predicate<TrustedContact>('isMe', '=', true)
name: string
contactUuid: string
publicKeySet: ContactPublicKeySetInterface
isMe: boolean
constructor(payload: DecryptedPayloadInterface<TrustedContactContent>) {
super(payload)
this.name = payload.content.name
this.contactUuid = payload.content.contactUuid
this.publicKeySet = ContactPublicKeySet.FromJson(payload.content.publicKeySet)
this.isMe = payload.content.isMe
}
override get isSingleton(): true {
return true
}
override singletonPredicate(): Predicate<TrustedContact> {
return TrustedContact.singletonPredicate
}
public findKeySet(params: {
targetEncryptionPublicKey: string
targetSigningPublicKey: string
}): FindPublicKeySetResult {
const set = this.publicKeySet.findKeySet(params)
if (!set) {
return undefined
}
return {
publicKeySet: set,
current: set === this.publicKeySet,
}
}
isPublicKeyTrusted(encryptionPublicKey: string): boolean {
const keySet = this.publicKeySet.findKeySetWithPublicKey(encryptionPublicKey)
if (keySet && !keySet.isRevoked) {
return true
}
return false
}
isSigningKeyTrusted(signingKey: string): boolean {
const keySet = this.publicKeySet.findKeySetWithSigningKey(signingKey)
if (keySet && !keySet.isRevoked) {
return true
}
return false
}
override strategyWhenConflictingWithItem(
_item: DecryptedItemInterface,
_previousRevision?: HistoryEntryInterface,
): ConflictStrategy {
return ConflictStrategy.KeepBase
}
}

View File

@@ -0,0 +1,11 @@
import { ItemContent } from '../../Abstract/Content/ItemContent'
import { ContactPublicKeySetInterface } from './PublicKeySet/ContactPublicKeySetInterface'
export type TrustedContactContentSpecialized = {
name: string
contactUuid: string
publicKeySet: ContactPublicKeySetInterface
isMe: boolean
}
export type TrustedContactContent = TrustedContactContentSpecialized & ItemContent

View File

@@ -0,0 +1,16 @@
import { DecryptedItemInterface } from '../../Abstract/Item/Interfaces/DecryptedItem'
import { FindPublicKeySetResult } from './PublicKeySet/FindPublicKeySetResult'
import { TrustedContactContent } from './TrustedContactContent'
import { ContactPublicKeySetInterface } from './PublicKeySet/ContactPublicKeySetInterface'
export interface TrustedContactInterface extends DecryptedItemInterface<TrustedContactContent> {
name: string
contactUuid: string
publicKeySet: ContactPublicKeySetInterface
isMe: boolean
findKeySet(params: { targetEncryptionPublicKey: string; targetSigningPublicKey: string }): FindPublicKeySetResult
isPublicKeyTrusted(encryptionPublicKey: string): boolean
isSigningKeyTrusted(signingKey: string): boolean
}

View File

@@ -0,0 +1,26 @@
import { DecryptedItemMutator } from '../../Abstract/Item'
import { TrustedContactContent } from './TrustedContactContent'
import { TrustedContactInterface } from './TrustedContactInterface'
import { ContactPublicKeySet } from './PublicKeySet/ContactPublicKeySet'
export class TrustedContactMutator extends DecryptedItemMutator<TrustedContactContent, TrustedContactInterface> {
set name(newName: string) {
this.mutableContent.name = newName
}
addPublicKey(params: { encryption: string; signing: string }): void {
const newKey = new ContactPublicKeySet(
params.encryption,
params.signing,
new Date(),
false,
this.immutableItem.publicKeySet,
)
this.mutableContent.publicKeySet = newKey
}
replacePublicKeySet(publicKeySet: ContactPublicKeySet): void {
this.mutableContent.publicKeySet = publicKeySet
}
}

View File

@@ -0,0 +1,62 @@
import { ConflictStrategy, DecryptedItem } from '../../Abstract/Item'
import { DecryptedPayloadInterface } from '../../Abstract/Payload'
import { HistoryEntryInterface } from '../../Runtime/History'
import { KeySystemRootKeyParamsInterface } from '../../Local/KeyParams/KeySystemRootKeyParamsInterface'
import { KeySystemRootKeyPasswordType } from '../../Local/KeyParams/KeySystemRootKeyPasswordType'
import { SharedVaultListingInterface, VaultListingInterface } from './VaultListingInterface'
import { VaultListingContent } from './VaultListingContent'
import { KeySystemRootKeyStorageMode } from '../KeySystemRootKey/KeySystemRootKeyStorageMode'
import { VaultListingSharingInfo } from './VaultListingSharingInfo'
import { KeySystemIdentifier } from '../KeySystemRootKey/KeySystemIdentifier'
export class VaultListing extends DecryptedItem<VaultListingContent> implements VaultListingInterface {
systemIdentifier: KeySystemIdentifier
rootKeyParams: KeySystemRootKeyParamsInterface
keyStorageMode: KeySystemRootKeyStorageMode
name: string
description?: string
sharing?: VaultListingSharingInfo
constructor(payload: DecryptedPayloadInterface<VaultListingContent>) {
super(payload)
this.systemIdentifier = payload.content.systemIdentifier
this.rootKeyParams = payload.content.rootKeyParams
this.keyStorageMode = payload.content.keyStorageMode
this.name = payload.content.name
this.description = payload.content.description
this.sharing = payload.content.sharing
}
override strategyWhenConflictingWithItem(
item: VaultListing,
_previousRevision?: HistoryEntryInterface,
): ConflictStrategy {
const baseKeyTimestamp = this.rootKeyParams.creationTimestamp
const incomingKeyTimestamp = item.rootKeyParams.creationTimestamp
return incomingKeyTimestamp > baseKeyTimestamp ? ConflictStrategy.KeepApply : ConflictStrategy.KeepBase
}
get keyPasswordType(): KeySystemRootKeyPasswordType {
return this.rootKeyParams.passwordType
}
isSharedVaultListing(): this is SharedVaultListingInterface {
return this.sharing != undefined
}
override get key_system_identifier(): undefined {
return undefined
}
override get shared_vault_uuid(): undefined {
return undefined
}
}

View File

@@ -0,0 +1,19 @@
import { ItemContent, SpecializedContent } from '../../Abstract/Content/ItemContent'
import { KeySystemIdentifier } from '../KeySystemRootKey/KeySystemIdentifier'
import { KeySystemRootKeyParamsInterface } from '../../Local/KeyParams/KeySystemRootKeyParamsInterface'
import { KeySystemRootKeyStorageMode } from '../KeySystemRootKey/KeySystemRootKeyStorageMode'
import { VaultListingSharingInfo } from './VaultListingSharingInfo'
export interface VaultListingContentSpecialized extends SpecializedContent {
systemIdentifier: KeySystemIdentifier
rootKeyParams: KeySystemRootKeyParamsInterface
keyStorageMode: KeySystemRootKeyStorageMode
name: string
description?: string
sharing?: VaultListingSharingInfo
}
export type VaultListingContent = VaultListingContentSpecialized & ItemContent

View File

@@ -0,0 +1,29 @@
import { KeySystemIdentifier } from '../KeySystemRootKey/KeySystemIdentifier'
import { KeySystemRootKeyParamsInterface } from '../../Local/KeyParams/KeySystemRootKeyParamsInterface'
import { KeySystemRootKeyPasswordType } from '../../Local/KeyParams/KeySystemRootKeyPasswordType'
import { KeySystemRootKeyStorageMode } from '../KeySystemRootKey/KeySystemRootKeyStorageMode'
import { VaultListingSharingInfo } from './VaultListingSharingInfo'
import { VaultListingContent } from './VaultListingContent'
import { DecryptedItemInterface } from '../../Abstract/Item'
export interface VaultListingInterface extends DecryptedItemInterface<VaultListingContent> {
systemIdentifier: KeySystemIdentifier
rootKeyParams: KeySystemRootKeyParamsInterface
keyStorageMode: KeySystemRootKeyStorageMode
name: string
description?: string
sharing?: VaultListingSharingInfo
get keyPasswordType(): KeySystemRootKeyPasswordType
isSharedVaultListing(): this is SharedVaultListingInterface
get key_system_identifier(): undefined
get shared_vault_uuid(): undefined
}
export interface SharedVaultListingInterface extends VaultListingInterface {
sharing: VaultListingSharingInfo
}

View File

@@ -0,0 +1,27 @@
import { DecryptedItemMutator } from '../../Abstract/Item'
import { KeySystemRootKeyParamsInterface } from '../../Local/KeyParams/KeySystemRootKeyParamsInterface'
import { KeySystemRootKeyStorageMode } from '../KeySystemRootKey/KeySystemRootKeyStorageMode'
import { VaultListingContent } from './VaultListingContent'
import { VaultListingSharingInfo } from './VaultListingSharingInfo'
export class VaultListingMutator extends DecryptedItemMutator<VaultListingContent> {
set name(name: string) {
this.mutableContent.name = name
}
set description(description: string | undefined) {
this.mutableContent.description = description
}
set sharing(sharing: VaultListingSharingInfo | undefined) {
this.mutableContent.sharing = sharing
}
set rootKeyParams(rootKeyParams: KeySystemRootKeyParamsInterface) {
this.mutableContent.rootKeyParams = rootKeyParams
}
set keyStorageMode(keyStorageMode: KeySystemRootKeyStorageMode) {
this.mutableContent.keyStorageMode = keyStorageMode
}
}

View File

@@ -0,0 +1,4 @@
export type VaultListingSharingInfo = {
sharedVaultUuid: string
ownerUserUuid: string
}

View File

@@ -23,6 +23,16 @@ import { NoteMutator } from '../../Syncable/Note/NoteMutator'
import { DecryptedItemInterface } from '../../Abstract/Item/Interfaces/DecryptedItem'
import { ItemContent } from '../../Abstract/Content/ItemContent'
import { DecryptedItemMutator } from '../../Abstract/Item/Mutator/DecryptedItemMutator'
import { DeletedItem } from '../../Abstract/Item/Implementations/DeletedItem'
import { EncryptedItemInterface } from '../../Abstract/Item/Interfaces/EncryptedItem'
import { DeletedItemInterface } from '../../Abstract/Item/Interfaces/DeletedItem'
import { SmartViewMutator } from '../../Syncable/SmartView'
import { TrustedContact } from '../../Syncable/TrustedContact/TrustedContact'
import { TrustedContactMutator } from '../../Syncable/TrustedContact/TrustedContactMutator'
import { KeySystemRootKey } from '../../Syncable/KeySystemRootKey/KeySystemRootKey'
import { KeySystemRootKeyMutator } from '../../Syncable/KeySystemRootKey/KeySystemRootKeyMutator'
import { VaultListing } from '../../Syncable/VaultListing/VaultListing'
import { VaultListingMutator } from '../../Syncable/VaultListing/VaultListingMutator'
import {
DeletedPayloadInterface,
EncryptedPayloadInterface,
@@ -30,10 +40,6 @@ import {
isDeletedPayload,
isEncryptedPayload,
} from '../../Abstract/Payload'
import { DeletedItem } from '../../Abstract/Item/Implementations/DeletedItem'
import { EncryptedItemInterface } from '../../Abstract/Item/Interfaces/EncryptedItem'
import { DeletedItemInterface } from '../../Abstract/Item/Interfaces/DeletedItem'
import { SmartViewMutator } from '../../Syncable/SmartView'
type ItemClass<C extends ItemContent = ItemContent> = new (payload: DecryptedPayloadInterface<C>) => DecryptedItem<C>
@@ -53,6 +59,9 @@ const ContentTypeClassMapping: Partial<Record<ContentType, MappingEntry>> = {
mutatorClass: ActionsExtensionMutator,
},
[ContentType.Component]: { itemClass: SNComponent, mutatorClass: ComponentMutator },
[ContentType.KeySystemRootKey]: { itemClass: KeySystemRootKey, mutatorClass: KeySystemRootKeyMutator },
[ContentType.TrustedContact]: { itemClass: TrustedContact, mutatorClass: TrustedContactMutator },
[ContentType.VaultListing]: { itemClass: VaultListing, mutatorClass: VaultListingMutator },
[ContentType.Editor]: { itemClass: SNEditor },
[ContentType.ExtensionRepo]: { itemClass: SNFeatureRepo },
[ContentType.File]: { itemClass: FileItem, mutatorClass: FileMutator },
@@ -65,13 +74,13 @@ const ContentTypeClassMapping: Partial<Record<ContentType, MappingEntry>> = {
export function CreateDecryptedMutatorForItem<
I extends DecryptedItemInterface,
M extends DecryptedItemMutator = DecryptedItemMutator,
M extends DecryptedItemMutator<ItemContent, I> = DecryptedItemMutator<ItemContent, I>,
>(item: I, type: MutationType): M {
const lookupValue = ContentTypeClassMapping[item.content_type]?.mutatorClass
if (lookupValue) {
return new lookupValue(item, type) as M
} else {
return new DecryptedItemMutator(item, type) as M
return new DecryptedItemMutator<ItemContent, I>(item, type) as M
}
}

View File

@@ -13,13 +13,13 @@ export const mockUuid = () => {
return `${currentId++}`
}
export const createNote = (payload?: Partial<NoteContent>): SNNote => {
export const createNote = (content?: Partial<NoteContent>): SNNote => {
return new SNNote(
new DecryptedPayload(
{
uuid: mockUuid(),
content_type: ContentType.Note,
content: FillItemContent({ ...payload }),
content: FillItemContent({ ...content }),
...PayloadTimestampDefaults(),
},
PayloadSource.Constructor,

View File

@@ -15,6 +15,7 @@ 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'
@@ -25,19 +26,26 @@ export * from './Abstract/Contextual/SessionHistory'
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/KeyParams/KeySystemRootKeyPasswordType'
export * from './Local/RootKey/KeychainTypes'
export * from './Local/RootKey/RootKeyContent'
export * from './Local/RootKey/RootKeyInterface'
export * from './Local/RootKey/RootKeyWithKeyPairsInterface'
export * from './Runtime/Collection/CollectionSort'
export * from './Runtime/Collection/Item/ItemCollection'
export * from './Runtime/Collection/Item/TagItemsIndex'
export * from './Runtime/Collection/Item/ItemCounter'
export * from './Runtime/Collection/Payload/ImmutablePayloadCollection'
export * from './Runtime/Collection/Payload/PayloadCollection'
export * from './Runtime/Deltas'
@@ -57,6 +65,20 @@ 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'
@@ -69,6 +91,30 @@ export * from './Syncable/SmartView'
export * from './Syncable/Tag'
export * from './Syncable/Theme'
export * from './Syncable/UserPrefs'
export * from './Syncable/TrustedContact/TrustedContact'
export * from './Syncable/TrustedContact/TrustedContactMutator'
export * from './Syncable/TrustedContact/TrustedContactContent'
export * from './Syncable/TrustedContact/TrustedContactInterface'
export * from './Syncable/TrustedContact/PublicKeySet/ContactPublicKeySetInterface'
export * from './Syncable/TrustedContact/PublicKeySet/ContactPublicKeySet'
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/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'
@@ -81,3 +127,4 @@ 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'