Quellcode durchsuchen

feat(syncing-server): add shared vault snjs filter (#677)

Co-authored-by: Mo <mo@standardnotes.com>
Karol Sójko vor 1 Jahr
Ursprung
Commit
b9bb83c0ce

+ 3 - 0
packages/syncing-server/src/Bootstrap/Container.ts

@@ -153,6 +153,7 @@ import { NotificationHttpRepresentation } from '../Mapping/Http/NotificationHttp
 import { DetermineSharedVaultOperationOnItem } from '../Domain/UseCase/SharedVaults/DetermineSharedVaultOperationOnItem/DetermineSharedVaultOperationOnItem'
 import { SharedVaultFilter } from '../Domain/Item/SaveRule/SharedVaultFilter'
 import { RemoveNotificationsForUser } from '../Domain/UseCase/Messaging/RemoveNotificationsForUser/RemoveNotificationsForUser'
+import { SharedVaultSnjsFilter } from '../Domain/Item/SaveRule/SharedVaultSnjsFilter'
 
 export class ContainerConfigLoader {
   private readonly DEFAULT_CONTENT_SIZE_TRANSFER_LIMIT = 10_000_000
@@ -515,6 +516,7 @@ export class ContainerConfigLoader {
           container.get(TYPES.Sync_SharedVaultUserRepository),
         ),
       )
+    container.bind<SharedVaultSnjsFilter>(TYPES.Sync_SharedVaultSnjsFilter).toConstantValue(new SharedVaultSnjsFilter())
     container
       .bind<ItemSaveValidatorInterface>(TYPES.Sync_ItemSaveValidator)
       .toConstantValue(
@@ -524,6 +526,7 @@ export class ContainerConfigLoader {
           container.get(TYPES.Sync_ContentTypeFilter),
           container.get(TYPES.Sync_ContentFilter),
           container.get(TYPES.Sync_SharedVaultFilter),
+          container.get(TYPES.Sync_SharedVaultSnjsFilter),
         ]),
       )
 

+ 1 - 0
packages/syncing-server/src/Bootstrap/Types.ts

@@ -101,6 +101,7 @@ const TYPES = {
   Sync_ItemSaveValidator: Symbol.for('Sync_ItemSaveValidator'),
   Sync_OwnershipFilter: Symbol.for('Sync_OwnershipFilter'),
   Sync_SharedVaultFilter: Symbol.for('Sync_SharedVaultFilter'),
+  Sync_SharedVaultSnjsFilter: Symbol.for('Sync_SharedVaultSnjsFilter'),
   Sync_TimeDifferenceFilter: Symbol.for('Sync_TimeDifferenceFilter'),
   Sync_ContentTypeFilter: Symbol.for('Sync_ContentTypeFilter'),
   Sync_ContentFilter: Symbol.for('Sync_ContentFilter'),

+ 2 - 0
packages/syncing-server/src/Domain/Item/SaveRule/ContentFilter.spec.ts

@@ -28,6 +28,7 @@ describe('ContentFilter', () => {
         apiVersion: ApiVersion.v20200115,
         itemHash,
         existingItem: null,
+        snjsVersion: '2.200.0',
       })
 
       expect(result).toEqual({
@@ -56,6 +57,7 @@ describe('ContentFilter', () => {
           shared_vault_uuid: null,
         }).getValue(),
         existingItem,
+        snjsVersion: '2.200.0',
       })
 
       expect(result).toEqual({

+ 2 - 0
packages/syncing-server/src/Domain/Item/SaveRule/ContentTypeFilter.spec.ts

@@ -35,6 +35,7 @@ describe('ContentTypeFilter', () => {
         apiVersion: ApiVersion.v20200115,
         itemHash,
         existingItem: null,
+        snjsVersion: '2.200.0',
       })
 
       expect(result).toEqual({
@@ -64,6 +65,7 @@ describe('ContentTypeFilter', () => {
         apiVersion: ApiVersion.v20200115,
         itemHash,
         existingItem,
+        snjsVersion: '2.200.0',
       })
 
       expect(result).toEqual({

+ 5 - 0
packages/syncing-server/src/Domain/Item/SaveRule/OwnershipFilter.spec.ts

@@ -48,6 +48,7 @@ describe('OwnershipFilter', () => {
       apiVersion: ApiVersion.v20200115,
       itemHash,
       existingItem,
+      snjsVersion: '2.200.0',
     })
 
     expect(result).toEqual({
@@ -77,6 +78,7 @@ describe('OwnershipFilter', () => {
       apiVersion: ApiVersion.v20200115,
       itemHash,
       existingItem,
+      snjsVersion: '2.200.0',
     })
 
     expect(result).toEqual({
@@ -101,6 +103,7 @@ describe('OwnershipFilter', () => {
         shared_vault_uuid: null,
       }).getValue(),
       existingItem,
+      snjsVersion: '2.200.0',
     })
 
     expect(result).toEqual({
@@ -125,6 +128,7 @@ describe('OwnershipFilter', () => {
         shared_vault_uuid: null,
       }).getValue(),
       existingItem: null,
+      snjsVersion: '2.200.0',
     })
 
     expect(result).toEqual({
@@ -150,6 +154,7 @@ describe('OwnershipFilter', () => {
       apiVersion: ApiVersion.v20200115,
       itemHash,
       existingItem,
+      snjsVersion: '2.200.0',
     })
 
     expect(result).toEqual({

+ 26 - 0
packages/syncing-server/src/Domain/Item/SaveRule/SharedVaultFilter.spec.ts

@@ -95,6 +95,7 @@ describe('SharedVaultFilter', () => {
       existingItem: existingItem,
       itemHash: itemHash,
       userUuid: '00000000-0000-0000-0000-000000000000',
+      snjsVersion: '2.200.0',
     })
 
     expect(result.passed).toBe(true)
@@ -109,6 +110,7 @@ describe('SharedVaultFilter', () => {
       existingItem: existingItem,
       itemHash: itemHash,
       userUuid: '00000000-0000-0000-0000-000000000000',
+      snjsVersion: '2.200.0',
     })
 
     expect(result.passed).toBe(false)
@@ -145,6 +147,7 @@ describe('SharedVaultFilter', () => {
       existingItem: existingItem,
       itemHash: itemHash,
       userUuid: '00000000-0000-0000-0000-000000000000',
+      snjsVersion: '2.200.0',
     })
 
     expect(result.passed).toBe(false)
@@ -179,6 +182,7 @@ describe('SharedVaultFilter', () => {
         existingItem: existingItem,
         itemHash: itemHash,
         userUuid: '00000000-0000-0000-0000-000000000000',
+        snjsVersion: '2.200.0',
       })
 
       expect(result.passed).toBe(false)
@@ -196,6 +200,7 @@ describe('SharedVaultFilter', () => {
         existingItem: existingItem,
         itemHash: itemHash,
         userUuid: '00000000-0000-0000-0000-000000000000',
+        snjsVersion: '2.200.0',
       })
 
       expect(result.passed).toBe(false)
@@ -210,6 +215,7 @@ describe('SharedVaultFilter', () => {
         existingItem: existingItem,
         itemHash: itemHash,
         userUuid: '00000000-0000-0000-0000-000000000000',
+        snjsVersion: '2.200.0',
       })
 
       expect(result.passed).toBe(true)
@@ -227,6 +233,7 @@ describe('SharedVaultFilter', () => {
         existingItem: existingItem,
         itemHash: itemHash,
         userUuid: '00000000-0000-0000-0000-000000000000',
+        snjsVersion: '2.200.0',
       })
 
       expect(result.passed).toBe(false)
@@ -274,6 +281,7 @@ describe('SharedVaultFilter', () => {
         existingItem: existingItem,
         itemHash: itemHash,
         userUuid: '00000000-0000-0000-0000-000000000000',
+        snjsVersion: '2.200.0',
       })
 
       expect(result.passed).toBe(false)
@@ -313,6 +321,7 @@ describe('SharedVaultFilter', () => {
         existingItem: existingItem,
         itemHash: itemHash,
         userUuid: '00000000-0000-0000-0000-000000000000',
+        snjsVersion: '2.200.0',
       })
 
       expect(result.passed).toBe(false)
@@ -351,6 +360,7 @@ describe('SharedVaultFilter', () => {
         existingItem: existingItem,
         itemHash: itemHash,
         userUuid: '00000000-0000-0000-0000-000000000000',
+        snjsVersion: '2.200.0',
       })
 
       expect(result.passed).toBe(false)
@@ -381,6 +391,7 @@ describe('SharedVaultFilter', () => {
         existingItem: existingItem,
         itemHash: itemHash,
         userUuid: '00000000-0000-0000-0000-000000000000',
+        snjsVersion: '2.200.0',
       })
 
       expect(result.passed).toBe(false)
@@ -395,6 +406,7 @@ describe('SharedVaultFilter', () => {
         existingItem: existingItem,
         itemHash: itemHash,
         userUuid: '00000000-0000-0000-0000-000000000000',
+        snjsVersion: '2.200.0',
       })
 
       expect(result.passed).toBe(true)
@@ -441,6 +453,7 @@ describe('SharedVaultFilter', () => {
         existingItem: existingItem,
         itemHash: itemHash,
         userUuid: '00000000-0000-0000-0000-000000000000',
+        snjsVersion: '2.200.0',
       })
 
       expect(result.passed).toBe(false)
@@ -470,6 +483,7 @@ describe('SharedVaultFilter', () => {
         existingItem: existingItem,
         itemHash: itemHash,
         userUuid: '00000000-0000-0000-0000-000000000001',
+        snjsVersion: '2.200.0',
       })
 
       expect(result.passed).toBe(false)
@@ -508,6 +522,7 @@ describe('SharedVaultFilter', () => {
         existingItem: existingItem,
         itemHash: itemHash,
         userUuid: '00000000-0000-0000-0000-000000000000',
+        snjsVersion: '2.200.0',
       })
 
       expect(result.passed).toBe(false)
@@ -538,6 +553,7 @@ describe('SharedVaultFilter', () => {
         existingItem: existingItem,
         itemHash: itemHash,
         userUuid: '00000000-0000-0000-0000-000000000000',
+        snjsVersion: '2.200.0',
       })
 
       expect(result.passed).toBe(false)
@@ -552,6 +568,7 @@ describe('SharedVaultFilter', () => {
         existingItem: existingItem,
         itemHash: itemHash,
         userUuid: '00000000-0000-0000-0000-000000000000',
+        snjsVersion: '2.200.0',
       })
 
       expect(result.passed).toBe(true)
@@ -598,6 +615,7 @@ describe('SharedVaultFilter', () => {
         existingItem: existingItem,
         itemHash: itemHash,
         userUuid: '00000000-0000-0000-0000-000000000000',
+        snjsVersion: '2.200.0',
       })
 
       expect(result.passed).toBe(false)
@@ -627,6 +645,7 @@ describe('SharedVaultFilter', () => {
         existingItem: existingItem,
         itemHash: itemHash,
         userUuid: '00000000-0000-0000-0000-000000000001',
+        snjsVersion: '2.200.0',
       })
 
       expect(result.passed).toBe(false)
@@ -665,6 +684,7 @@ describe('SharedVaultFilter', () => {
         existingItem: existingItem,
         itemHash: itemHash,
         userUuid: '00000000-0000-0000-0000-000000000000',
+        snjsVersion: '2.200.0',
       })
 
       expect(result.passed).toBe(false)
@@ -695,6 +715,7 @@ describe('SharedVaultFilter', () => {
         existingItem: existingItem,
         itemHash: itemHash,
         userUuid: '00000000-0000-0000-0000-000000000000',
+        snjsVersion: '2.200.0',
       })
 
       expect(result.passed).toBe(false)
@@ -709,6 +730,7 @@ describe('SharedVaultFilter', () => {
         existingItem: existingItem,
         itemHash: itemHash,
         userUuid: '00000000-0000-0000-0000-000000000000',
+        snjsVersion: '2.200.0',
       })
 
       expect(result.passed).toBe(true)
@@ -730,6 +752,7 @@ describe('SharedVaultFilter', () => {
         existingItem: existingItem,
         itemHash: itemHash,
         userUuid: '00000000-0000-0000-0000-000000000000',
+        snjsVersion: '2.200.0',
       })
 
       expect(result.passed).toBe(false)
@@ -760,6 +783,7 @@ describe('SharedVaultFilter', () => {
         existingItem: existingItem,
         itemHash: itemHash,
         userUuid: '00000000-0000-0000-0000-000000000000',
+        snjsVersion: '2.200.0',
       })
 
       expect(result.passed).toBe(false)
@@ -774,6 +798,7 @@ describe('SharedVaultFilter', () => {
         existingItem: existingItem,
         itemHash: itemHash,
         userUuid: '00000000-0000-0000-0000-000000000000',
+        snjsVersion: '2.200.0',
       })
 
       expect(result.passed).toBe(true)
@@ -811,6 +836,7 @@ describe('SharedVaultFilter', () => {
         existingItem: existingItem,
         itemHash: itemHash,
         userUuid: '00000000-0000-0000-0000-000000000000',
+        snjsVersion: '2.200.0',
       })
 
       expect(result.passed).toBe(false)

+ 129 - 0
packages/syncing-server/src/Domain/Item/SaveRule/SharedVaultSnjsFilter.spec.ts

@@ -0,0 +1,129 @@
+import { Uuid, ContentType, Dates, Timestamps, UniqueEntityId } from '@standardnotes/domain-core'
+
+import { SharedVaultAssociation } from '../../SharedVault/SharedVaultAssociation'
+import { Item } from '../Item'
+import { ItemHash } from '../ItemHash'
+import { SharedVaultSnjsFilter } from './SharedVaultSnjsFilter'
+
+describe('SharedVaultSnjsFilter', () => {
+  const createFilter = () => new SharedVaultSnjsFilter()
+
+  let itemHash: ItemHash
+  let existingItem: Item
+
+  beforeEach(() => {
+    existingItem = Item.create(
+      {
+        userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+        updatedWithSession: null,
+        content: 'foobar',
+        contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
+        encItemKey: null,
+        authHash: null,
+        itemsKeyId: null,
+        duplicateOf: null,
+        deleted: false,
+        dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
+        timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
+        sharedVaultAssociation: SharedVaultAssociation.create({
+          itemUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+          lastEditedBy: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+          sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+          timestamps: Timestamps.create(123, 123).getValue(),
+        }).getValue(),
+      },
+      new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
+    ).getValue()
+
+    itemHash = ItemHash.create({
+      uuid: '2-3-4',
+      content_type: ContentType.TYPES.Note,
+      user_uuid: '00000000-0000-0000-0000-000000000000',
+      content: 'foobar',
+      created_at: '2020-01-01T00:00:00.000Z',
+      updated_at: '2020-01-01T00:00:00.000Z',
+      created_at_timestamp: 123,
+      updated_at_timestamp: 123,
+      key_system_identifier: 'key-system-identifier',
+      shared_vault_uuid: '00000000-0000-0000-0000-000000000000',
+    }).getValue()
+  })
+
+  it('should filter out items with invalid snjs version', async () => {
+    const result = await createFilter().check({
+      userUuid: '00000000-0000-0000-0000-000000000001',
+      apiVersion: '20200115',
+      itemHash,
+      existingItem,
+      snjsVersion: '2.100.0',
+    })
+
+    expect(result).toEqual({
+      passed: false,
+      conflict: {
+        unsavedItem: itemHash,
+        serverItem: existingItem,
+        type: 'shared_vault_snjs_version_error',
+      },
+    })
+  })
+
+  it('should filter out item hashes with invalid snjs version', async () => {
+    const result = await createFilter().check({
+      userUuid: '00000000-0000-0000-0000-000000000001',
+      apiVersion: '20200115',
+      itemHash,
+      existingItem: null,
+      snjsVersion: '2.100.0',
+    })
+
+    expect(result).toEqual({
+      passed: false,
+      conflict: {
+        unsavedItem: itemHash,
+        type: 'shared_vault_snjs_version_error',
+      },
+    })
+  })
+
+  it('should leave items with valid snjs version', async () => {
+    const result = await createFilter().check({
+      userUuid: '00000000-0000-0000-0000-000000000001',
+      apiVersion: '20200115',
+      itemHash,
+      existingItem,
+      snjsVersion: '2.200.0',
+    })
+
+    expect(result).toEqual({
+      passed: true,
+    })
+  })
+
+  it('should leave items that are not associated with a shared vault and not hashed with a key system', async () => {
+    existingItem = Item.create(
+      {
+        ...existingItem.props,
+        sharedVaultAssociation: undefined,
+      },
+      new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
+    ).getValue()
+
+    itemHash = ItemHash.create({
+      ...itemHash.props,
+      key_system_identifier: null,
+    }).getValue()
+
+    const result = await createFilter().check({
+      userUuid: '00000000-0000-0000-0000-000000000001',
+      apiVersion: '20200115',
+      itemHash,
+      existingItem,
+      snjsVersion: '2.200.0',
+    })
+
+    expect(result).toEqual({
+      passed: true,
+    })
+  })
+})

+ 39 - 0
packages/syncing-server/src/Domain/Item/SaveRule/SharedVaultSnjsFilter.ts

@@ -0,0 +1,39 @@
+import { lt } from 'semver'
+import { ConflictType } from '@standardnotes/responses'
+
+import { ItemSaveValidationDTO } from '../SaveValidator/ItemSaveValidationDTO'
+import { ItemSaveRuleResult } from './ItemSaveRuleResult'
+import { ItemSaveRuleInterface } from './ItemSaveRuleInterface'
+
+export class SharedVaultSnjsFilter implements ItemSaveRuleInterface {
+  private readonly MINIMUM_SNJS_VERSION_FOR_SHARED_VAULT_OPERATIONS = '2.200.0'
+
+  async check(dto: ItemSaveValidationDTO): Promise<ItemSaveRuleResult> {
+    const isItemInASharedVaultOrHasKeySystem =
+      dto.existingItem?.isAssociatedWithASharedVault() || dto.itemHash.hasDedicatedKeySystemAssociation()
+    if (!isItemInASharedVaultOrHasKeySystem) {
+      return {
+        passed: true,
+      }
+    }
+
+    const isClientSnjsVersionLessThanMinimum = lt(
+      dto.snjsVersion,
+      this.MINIMUM_SNJS_VERSION_FOR_SHARED_VAULT_OPERATIONS,
+    )
+    if (isClientSnjsVersionLessThanMinimum) {
+      return {
+        passed: false,
+        conflict: {
+          unsavedItem: dto.itemHash,
+          serverItem: dto.existingItem ?? undefined,
+          type: ConflictType.SharedVaultSnjsVersionError,
+        },
+      }
+    }
+
+    return {
+      passed: true,
+    }
+  }
+}

+ 8 - 0
packages/syncing-server/src/Domain/Item/SaveRule/TimeDifferenceFilter.spec.ts

@@ -67,6 +67,7 @@ describe('TimeDifferenceFilter', () => {
     const result = await createFilter().check({
       userUuid: '1-2-3',
       apiVersion: ApiVersion.v20200115,
+      snjsVersion: '2.200.0',
       itemHash,
       existingItem: null,
     })
@@ -86,6 +87,7 @@ describe('TimeDifferenceFilter', () => {
     const result = await createFilter().check({
       userUuid: '1-2-3',
       apiVersion: ApiVersion.v20161215,
+      snjsVersion: '2.200.0',
       itemHash,
       existingItem,
     })
@@ -104,6 +106,7 @@ describe('TimeDifferenceFilter', () => {
     const result = await createFilter().check({
       userUuid: '1-2-3',
       apiVersion: ApiVersion.v20200115,
+      snjsVersion: '2.200.0',
       itemHash,
       existingItem,
     })
@@ -126,6 +129,7 @@ describe('TimeDifferenceFilter', () => {
     const result = await createFilter().check({
       userUuid: '1-2-3',
       apiVersion: ApiVersion.v20200115,
+      snjsVersion: '2.200.0',
       itemHash,
       existingItem,
     })
@@ -153,6 +157,7 @@ describe('TimeDifferenceFilter', () => {
       apiVersion: ApiVersion.v20161215,
       itemHash,
       existingItem,
+      snjsVersion: '2.200.0',
     })
 
     expect(result).toEqual({
@@ -178,6 +183,7 @@ describe('TimeDifferenceFilter', () => {
       apiVersion: ApiVersion.v20161215,
       itemHash,
       existingItem,
+      snjsVersion: '2.200.0',
     })
 
     expect(result).toEqual({
@@ -201,6 +207,7 @@ describe('TimeDifferenceFilter', () => {
     const result = await createFilter().check({
       userUuid: '1-2-3',
       apiVersion: ApiVersion.v20200115,
+      snjsVersion: '2.200.0',
       itemHash,
       existingItem,
     })
@@ -226,6 +233,7 @@ describe('TimeDifferenceFilter', () => {
     const result = await createFilter().check({
       userUuid: '1-2-3',
       apiVersion: ApiVersion.v20200115,
+      snjsVersion: '2.200.0',
       itemHash,
       existingItem,
     })

+ 1 - 0
packages/syncing-server/src/Domain/Item/SaveValidator/ItemSaveValidationDTO.ts

@@ -5,5 +5,6 @@ export type ItemSaveValidationDTO = {
   userUuid: string
   apiVersion: string
   itemHash: ItemHash
+  snjsVersion: string
   existingItem: Item | null
 }

+ 2 - 0
packages/syncing-server/src/Domain/Item/SaveValidator/ItemSaveValidator.spec.ts

@@ -24,6 +24,7 @@ describe('ItemSaveValidator', () => {
       userUuid: '1-2-3',
       itemHash,
       existingItem: null,
+      snjsVersion: '2.200.0',
     })
 
     expect(result).toEqual({
@@ -39,6 +40,7 @@ describe('ItemSaveValidator', () => {
       userUuid: '1-2-3',
       itemHash,
       existingItem: null,
+      snjsVersion: '2.200.0',
     })
 
     expect(result).toEqual({

+ 10 - 0
packages/syncing-server/src/Domain/UseCase/Syncing/SaveItems/SaveItems.spec.ts

@@ -82,6 +82,7 @@ describe('SaveItems', () => {
       apiVersion: '1',
       readOnlyAccess: false,
       sessionUuid: 'session-uuid',
+      snjsVersion: '2.200.0',
     })
 
     expect(result.isFailed()).toBeFalsy()
@@ -104,6 +105,7 @@ describe('SaveItems', () => {
       apiVersion: '1',
       readOnlyAccess: false,
       sessionUuid: 'session-uuid',
+      snjsVersion: '2.200.0',
     })
 
     expect(result.isFailed()).toBeFalsy()
@@ -126,6 +128,7 @@ describe('SaveItems', () => {
       apiVersion: '1',
       readOnlyAccess: false,
       sessionUuid: 'session-uuid',
+      snjsVersion: '2.200.0',
     })
 
     expect(result.isFailed()).toBeFalsy()
@@ -146,6 +149,7 @@ describe('SaveItems', () => {
       apiVersion: '1',
       readOnlyAccess: true,
       sessionUuid: 'session-uuid',
+      snjsVersion: '2.200.0',
     })
 
     expect(result.isFailed()).toBeFalsy()
@@ -167,6 +171,7 @@ describe('SaveItems', () => {
       apiVersion: '1',
       readOnlyAccess: false,
       sessionUuid: 'session-uuid',
+      snjsVersion: '2.200.0',
     })
 
     expect(result.isFailed()).toBeFalsy()
@@ -184,6 +189,7 @@ describe('SaveItems', () => {
       apiVersion: '1',
       readOnlyAccess: false,
       sessionUuid: 'session-uuid',
+      snjsVersion: '2.200.0',
     })
 
     expect(result.isFailed()).toBeFalsy()
@@ -201,6 +207,7 @@ describe('SaveItems', () => {
       apiVersion: '1',
       readOnlyAccess: false,
       sessionUuid: 'session-uuid',
+      snjsVersion: '2.200.0',
     })
 
     expect(result.isFailed()).toBeFalsy()
@@ -224,6 +231,7 @@ describe('SaveItems', () => {
       apiVersion: '1',
       readOnlyAccess: false,
       sessionUuid: 'session-uuid',
+      snjsVersion: '2.200.0',
     })
 
     expect(result.isFailed()).toBeFalsy()
@@ -247,6 +255,7 @@ describe('SaveItems', () => {
       apiVersion: '1',
       readOnlyAccess: false,
       sessionUuid: 'session-uuid',
+      snjsVersion: '2.200.0',
     })
 
     expect(result.isFailed()).toBeFalsy()
@@ -291,6 +300,7 @@ describe('SaveItems', () => {
       apiVersion: '2',
       readOnlyAccess: false,
       sessionUuid: 'session-uuid',
+      snjsVersion: '2.200.0',
     })
 
     expect(result.isFailed()).toBeFalsy()

+ 1 - 0
packages/syncing-server/src/Domain/UseCase/Syncing/SaveItems/SaveItems.ts

@@ -59,6 +59,7 @@ export class SaveItems implements UseCaseInterface<SaveItemsResult> {
         apiVersion: dto.apiVersion,
         itemHash,
         existingItem,
+        snjsVersion: dto.snjsVersion,
       })
       if (!processingResult.passed) {
         if (processingResult.conflict) {

+ 1 - 0
packages/syncing-server/src/Domain/UseCase/Syncing/SaveItems/SaveItemsDTO.ts

@@ -6,4 +6,5 @@ export interface SaveItemsDTO {
   apiVersion: string
   readOnlyAccess: boolean
   sessionUuid: string | null
+  snjsVersion: string
 }

+ 1 - 0
packages/syncing-server/src/Domain/UseCase/Syncing/SyncItems/SyncItems.spec.ts

@@ -172,6 +172,7 @@ describe('SyncItems', () => {
       itemHashes: [itemHash],
       userUuid: '1-2-3',
       apiVersion: '20200115',
+      snjsVersion: '1.2.3',
       readOnlyAccess: false,
       sessionUuid: null,
     })

+ 1 - 0
packages/syncing-server/src/Domain/UseCase/Syncing/SyncItems/SyncItems.ts

@@ -43,6 +43,7 @@ export class SyncItems implements UseCaseInterface<SyncItemsResponse> {
       apiVersion: dto.apiVersion,
       readOnlyAccess: dto.readOnlyAccess,
       sessionUuid: dto.sessionUuid,
+      snjsVersion: dto.snjsVersion,
     })
     if (saveItemsResultOrError.isFailed()) {
       return Result.fail(saveItemsResultOrError.getError())