Kaynağa Gözat

feat: should be able to access shared item revisions as third party user (#807)

Karol Sójko 1 yıl önce
ebeveyn
işleme
794cd8734a
92 değiştirilmiş dosya ile 1726 ekleme ve 266 silme
  1. 1 0
      packages/api-gateway/src/Controller/AuthMiddleware.ts
  2. 15 0
      packages/auth/migrations/mysql/1694000575425-add-shared-vault-users.ts
  3. 21 0
      packages/auth/migrations/sqlite/1694000640645-add-shared-vault-users.ts
  4. 55 0
      packages/auth/src/Bootstrap/Container.ts
  5. 2 0
      packages/auth/src/Bootstrap/DataSource.ts
  6. 7 0
      packages/auth/src/Bootstrap/Types.ts
  7. 24 0
      packages/auth/src/Domain/Handler/UserAddedToSharedVaultEventHandler.ts
  8. 22 0
      packages/auth/src/Domain/Handler/UserRemovedFromSharedVaultEventHandler.ts
  9. 8 0
      packages/auth/src/Domain/SharedVault/SharedVaultUserRepositoryInterface.ts
  10. 106 0
      packages/auth/src/Domain/UseCase/AddSharedVaultUser/AddSharedVaultUser.spec.ts
  11. 56 0
      packages/auth/src/Domain/UseCase/AddSharedVaultUser/AddSharedVaultUser.ts
  12. 7 0
      packages/auth/src/Domain/UseCase/AddSharedVaultUser/AddSharedVaultUserDTO.ts
  13. 44 1
      packages/auth/src/Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken.spec.ts
  14. 10 0
      packages/auth/src/Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken.ts
  15. 68 0
      packages/auth/src/Domain/UseCase/RemoveSharedVaultUser/RemoveSharedVaultUser.spec.ts
  16. 34 0
      packages/auth/src/Domain/UseCase/RemoveSharedVaultUser/RemoveSharedVaultUser.ts
  17. 4 0
      packages/auth/src/Domain/UseCase/RemoveSharedVaultUser/RemoveSharedVaultUserDTO.ts
  18. 40 0
      packages/auth/src/Infra/TypeORM/TypeORMSharedVaultUser.ts
  19. 54 0
      packages/auth/src/Infra/TypeORM/TypeORMSharedVaultUserRepository.ts
  20. 67 0
      packages/auth/src/Mapping/SharedVaultUserPersistenceMapper.ts
  21. 23 0
      packages/domain-core/src/Domain/Common/Timestamps.spec.ts
  22. 2 2
      packages/domain-core/src/Domain/Common/Timestamps.ts
  23. 3 2
      packages/domain-core/src/Domain/SharedVault/SharedVaultUser.spec.ts
  24. 3 2
      packages/domain-core/src/Domain/SharedVault/SharedVaultUser.ts
  25. 10 0
      packages/domain-core/src/Domain/SharedVault/SharedVaultUserProps.ts
  26. 2 0
      packages/domain-core/src/Domain/index.ts
  27. 7 0
      packages/domain-events/src/Domain/Event/UserAddedToSharedVaultEvent.ts
  28. 7 0
      packages/domain-events/src/Domain/Event/UserAddedToSharedVaultEventPayload.ts
  29. 7 0
      packages/domain-events/src/Domain/Event/UserRemovedFromSharedVaultEvent.ts
  30. 4 0
      packages/domain-events/src/Domain/Event/UserRemovedFromSharedVaultEventPayload.ts
  31. 4 0
      packages/domain-events/src/Domain/index.ts
  32. 41 0
      packages/revisions/migrations/mysql-legacy/1669113322388-init.ts
  33. 28 0
      packages/revisions/migrations/mysql-legacy/1669636497932-remove-date-indexes.ts
  34. 13 0
      packages/revisions/migrations/mysql-legacy/1669735585016-make-user-uuid-nullable.ts
  35. 17 0
      packages/revisions/migrations/mysql/1693915383950-add-shared-vault-information.ts
  36. 37 0
      packages/revisions/migrations/sqlite/1693915775491-add-shared-vault-information.ts
  37. 41 13
      packages/revisions/src/Bootstrap/Container.ts
  38. 13 3
      packages/revisions/src/Bootstrap/DataSource.ts
  39. 7 0
      packages/revisions/src/Bootstrap/MigrationsDataSource.ts
  40. 5 0
      packages/revisions/src/Bootstrap/Types.ts
  41. 3 0
      packages/revisions/src/Domain/KeySystem/KeySystemAssocationProps.ts
  42. 15 0
      packages/revisions/src/Domain/KeySystem/KeySystemAssociation.spec.ts
  43. 18 0
      packages/revisions/src/Domain/KeySystem/KeySystemAssociation.ts
  44. 5 0
      packages/revisions/src/Domain/Revision/RevisionProps.ts
  45. 1 1
      packages/revisions/src/Domain/Revision/RevisionRepositoryInterface.ts
  46. 14 0
      packages/revisions/src/Domain/SharedVault/SharedVaultAssociation.spec.ts
  47. 13 0
      packages/revisions/src/Domain/SharedVault/SharedVaultAssociation.ts
  48. 6 0
      packages/revisions/src/Domain/SharedVault/SharedVaultAssociationProps.ts
  49. 15 0
      packages/revisions/src/Domain/UseCase/GetRevisionsMetada/GetRevisionsMetada.spec.ts
  50. 10 0
      packages/revisions/src/Domain/UseCase/GetRevisionsMetada/GetRevisionsMetada.ts
  51. 1 0
      packages/revisions/src/Domain/UseCase/GetRevisionsMetada/GetRevisionsMetadaDTO.ts
  52. 3 0
      packages/revisions/src/Infra/InversifyExpress/Base/BaseRevisionsController.ts
  53. 1 0
      packages/revisions/src/Infra/InversifyExpress/Middleware/ApiGatewayAuthMiddleware.ts
  54. 10 0
      packages/revisions/src/Infra/TypeORM/MongoDB/MongoDBRevision.ts
  55. 30 10
      packages/revisions/src/Infra/TypeORM/MongoDB/MongoDBRevisionRepository.ts
  56. 83 0
      packages/revisions/src/Infra/TypeORM/SQL/SQLLegacyRevision.ts
  57. 146 0
      packages/revisions/src/Infra/TypeORM/SQL/SQLLegacyRevisionRepository.ts
  58. 14 66
      packages/revisions/src/Infra/TypeORM/SQL/SQLRevision.ts
  59. 23 110
      packages/revisions/src/Infra/TypeORM/SQL/SQLRevisionRepository.ts
  60. 37 0
      packages/revisions/src/Mapping/Backup/RevisionItemStringMapper.ts
  61. 43 0
      packages/revisions/src/Mapping/Persistence/SQL/SQLLegacyRevisionMetadataPersistenceMapper.ts
  62. 73 0
      packages/revisions/src/Mapping/Persistence/SQL/SQLLegacyRevisionPersistenceMapper.ts
  63. 46 0
      packages/revisions/src/Mapping/Persistence/SQL/SQLRevisionPersistenceMapper.ts
  64. 4 0
      packages/security/src/Domain/Token/CrossServiceTokenData.ts
  65. 16 8
      packages/syncing-server/src/Bootstrap/Container.ts
  66. 40 0
      packages/syncing-server/src/Domain/Event/DomainEventFactory.ts
  67. 13 0
      packages/syncing-server/src/Domain/Event/DomainEventFactoryInterface.ts
  68. 1 1
      packages/syncing-server/src/Domain/Item/SaveRule/SharedVaultFilter.spec.ts
  69. 0 8
      packages/syncing-server/src/Domain/SharedVault/User/SharedVaultUserProps.ts
  70. 1 3
      packages/syncing-server/src/Domain/SharedVault/User/SharedVaultUserRepositoryInterface.ts
  71. 1 1
      packages/syncing-server/src/Domain/UseCase/Messaging/AddNotificationsForUsers/AddNotificationsForUsers.spec.ts
  72. 22 3
      packages/syncing-server/src/Domain/UseCase/SharedVaults/AddUserToSharedVault/AddUserToSharedVault.spec.ts
  73. 22 2
      packages/syncing-server/src/Domain/UseCase/SharedVaults/AddUserToSharedVault/AddUserToSharedVault.ts
  74. 2 1
      packages/syncing-server/src/Domain/UseCase/SharedVaults/CreateSharedVault/CreateSharedVaultResult.ts
  75. 1 2
      packages/syncing-server/src/Domain/UseCase/SharedVaults/CreateSharedVaultFileValetToken/CreateSharedVaultFileValetToken.spec.ts
  76. 1 2
      packages/syncing-server/src/Domain/UseCase/SharedVaults/DeleteSharedVault/DeleteSharedVault.spec.ts
  77. 2 2
      packages/syncing-server/src/Domain/UseCase/SharedVaults/GetSharedVaultUsers/GetSharedVaultUsers.spec.ts
  78. 2 2
      packages/syncing-server/src/Domain/UseCase/SharedVaults/GetSharedVaultUsers/GetSharedVaultUsers.ts
  79. 2 2
      packages/syncing-server/src/Domain/UseCase/SharedVaults/GetSharedVaults/GetSharedVaults.spec.ts
  80. 2 3
      packages/syncing-server/src/Domain/UseCase/SharedVaults/InviteUserToSharedVault/InviteUserToSharedVault.spec.ts
  81. 27 3
      packages/syncing-server/src/Domain/UseCase/SharedVaults/RemoveUserFromSharedVault/RemoveUserFromSharedVault.spec.ts
  82. 11 0
      packages/syncing-server/src/Domain/UseCase/SharedVaults/RemoveUserFromSharedVault/RemoveUserFromSharedVault.ts
  83. 1 2
      packages/syncing-server/src/Infra/InversifyExpressUtils/AnnotatedSharedVaultUsersController.ts
  84. 1 2
      packages/syncing-server/src/Infra/InversifyExpressUtils/AnnotatedSharedVaultsController.ts
  85. 2 0
      packages/syncing-server/src/Infra/InversifyExpressUtils/Base/BaseSharedVaultInvitesController.ts
  86. 3 2
      packages/syncing-server/src/Infra/InversifyExpressUtils/Base/BaseSharedVaultUsersController.ts
  87. 5 2
      packages/syncing-server/src/Infra/InversifyExpressUtils/Base/BaseSharedVaultsController.ts
  88. 1 2
      packages/syncing-server/src/Infra/TypeORM/TypeORMSharedVaultUserRepository.ts
  89. 9 0
      packages/syncing-server/src/Mapping/Backup/ItemBackupMapper.ts
  90. 3 0
      packages/syncing-server/src/Mapping/Backup/ItemBackupRepresentation.ts
  91. 1 2
      packages/syncing-server/src/Mapping/Http/SharedVaultUserHttpMapper.ts
  92. 1 1
      packages/syncing-server/src/Mapping/Persistence/SharedVaultUserPersistenceMapper.ts

+ 1 - 0
packages/api-gateway/src/Controller/AuthMiddleware.ts

@@ -72,6 +72,7 @@ export abstract class AuthMiddleware extends BaseMiddleware {
       response.locals.session = decodedToken.session
       response.locals.roles = decodedToken.roles
       response.locals.sharedVaultOwnerContext = decodedToken.shared_vault_owner_context
+      response.locals.belongsToSharedVaults = decodedToken.belongs_to_shared_vaults ?? []
     } catch (error) {
       const errorMessage = (error as AxiosError).isAxiosError
         ? JSON.stringify((error as AxiosError).response?.data)

+ 15 - 0
packages/auth/migrations/mysql/1694000575425-add-shared-vault-users.ts

@@ -0,0 +1,15 @@
+import { MigrationInterface, QueryRunner } from 'typeorm'
+
+export class AddSharedVaultUsers1694000575425 implements MigrationInterface {
+  public async up(queryRunner: QueryRunner): Promise<void> {
+    await queryRunner.query(
+      'CREATE TABLE `auth_shared_vault_users` (`uuid` varchar(36) NOT NULL, `shared_vault_uuid` varchar(36) NOT NULL, `user_uuid` varchar(36) NOT NULL, `permission` varchar(24) NOT NULL, `created_at_timestamp` bigint NOT NULL, `updated_at_timestamp` bigint NOT NULL, INDEX `shared_vault_uuid_on_auth_shared_vault_users` (`shared_vault_uuid`), INDEX `user_uuid_on_auth_shared_vault_users` (`user_uuid`), PRIMARY KEY (`uuid`)) ENGINE=InnoDB',
+    )
+  }
+
+  public async down(queryRunner: QueryRunner): Promise<void> {
+    await queryRunner.query('DROP INDEX `user_uuid_on_auth_shared_vault_users` ON `auth_shared_vault_users`')
+    await queryRunner.query('DROP INDEX `shared_vault_uuid_on_auth_shared_vault_users` ON `auth_shared_vault_users`')
+    await queryRunner.query('DROP TABLE `auth_shared_vault_users`')
+  }
+}

+ 21 - 0
packages/auth/migrations/sqlite/1694000640645-add-shared-vault-users.ts

@@ -0,0 +1,21 @@
+import { MigrationInterface, QueryRunner } from 'typeorm'
+
+export class AddSharedVaultUsers1694000640645 implements MigrationInterface {
+  public async up(queryRunner: QueryRunner): Promise<void> {
+    await queryRunner.query(
+      'CREATE TABLE "auth_shared_vault_users" ("uuid" varchar PRIMARY KEY NOT NULL, "shared_vault_uuid" varchar(36) NOT NULL, "user_uuid" varchar(36) NOT NULL, "permission" varchar(24) NOT NULL, "created_at_timestamp" bigint NOT NULL, "updated_at_timestamp" bigint NOT NULL)',
+    )
+    await queryRunner.query(
+      'CREATE INDEX "shared_vault_uuid_on_auth_shared_vault_users" ON "auth_shared_vault_users" ("shared_vault_uuid") ',
+    )
+    await queryRunner.query(
+      'CREATE INDEX "user_uuid_on_auth_shared_vault_users" ON "auth_shared_vault_users" ("user_uuid") ',
+    )
+  }
+
+  public async down(queryRunner: QueryRunner): Promise<void> {
+    await queryRunner.query('DROP INDEX "user_uuid_on_auth_shared_vault_users"')
+    await queryRunner.query('DROP INDEX "shared_vault_uuid_on_auth_shared_vault_users"')
+    await queryRunner.query('DROP TABLE "auth_shared_vault_users"')
+  }
+}

+ 55 - 0
packages/auth/src/Bootstrap/Container.ts

@@ -189,6 +189,7 @@ import {
   ControllerContainer,
   ControllerContainerInterface,
   MapperInterface,
+  SharedVaultUser,
 } from '@standardnotes/domain-core'
 import { SessionTracePersistenceMapper } from '../Mapping/SessionTracePersistenceMapper'
 import { SessionTrace } from '../Domain/Session/SessionTrace'
@@ -263,6 +264,14 @@ import { InMemoryTransitionStatusRepository } from '../Infra/InMemory/InMemoryTr
 import { TransitionStatusUpdatedEventHandler } from '../Domain/Handler/TransitionStatusUpdatedEventHandler'
 import { UpdateTransitionStatus } from '../Domain/UseCase/UpdateTransitionStatus/UpdateTransitionStatus'
 import { GetTransitionStatus } from '../Domain/UseCase/GetTransitionStatus/GetTransitionStatus'
+import { TypeORMSharedVaultUser } from '../Infra/TypeORM/TypeORMSharedVaultUser'
+import { SharedVaultUserPersistenceMapper } from '../Mapping/SharedVaultUserPersistenceMapper'
+import { SharedVaultUserRepositoryInterface } from '../Domain/SharedVault/SharedVaultUserRepositoryInterface'
+import { TypeORMSharedVaultUserRepository } from '../Infra/TypeORM/TypeORMSharedVaultUserRepository'
+import { AddSharedVaultUser } from '../Domain/UseCase/AddSharedVaultUser/AddSharedVaultUser'
+import { RemoveSharedVaultUser } from '../Domain/UseCase/RemoveSharedVaultUser/RemoveSharedVaultUser'
+import { UserAddedToSharedVaultEventHandler } from '../Domain/Handler/UserAddedToSharedVaultEventHandler'
+import { UserRemovedFromSharedVaultEventHandler } from '../Domain/Handler/UserRemovedFromSharedVaultEventHandler'
 
 export class ContainerConfigLoader {
   async load(configuration?: {
@@ -372,6 +381,9 @@ export class ContainerConfigLoader {
     container
       .bind<MapperInterface<CacheEntry, TypeORMCacheEntry>>(TYPES.Auth_CacheEntryPersistenceMapper)
       .toConstantValue(new CacheEntryPersistenceMapper())
+    container
+      .bind<MapperInterface<SharedVaultUser, TypeORMSharedVaultUser>>(TYPES.Auth_SharedVaultUserPersistenceMapper)
+      .toConstantValue(new SharedVaultUserPersistenceMapper())
 
     // ORM
     container
@@ -412,6 +424,9 @@ export class ContainerConfigLoader {
     container
       .bind<Repository<TypeORMCacheEntry>>(TYPES.Auth_ORMCacheEntryRepository)
       .toConstantValue(appDataSource.getRepository(TypeORMCacheEntry))
+    container
+      .bind<Repository<TypeORMSharedVaultUser>>(TYPES.Auth_ORMSharedVaultUserRepository)
+      .toConstantValue(appDataSource.getRepository(TypeORMSharedVaultUser))
 
     // Repositories
     container.bind<SessionRepositoryInterface>(TYPES.Auth_SessionRepository).to(TypeORMSessionRepository)
@@ -468,6 +483,16 @@ export class ContainerConfigLoader {
           container.get(TYPES.Auth_CacheEntryPersistenceMapper),
         ),
       )
+    container
+      .bind<SharedVaultUserRepositoryInterface>(TYPES.Auth_SharedVaultUserRepository)
+      .toConstantValue(
+        new TypeORMSharedVaultUserRepository(
+          container.get<Repository<TypeORMSharedVaultUser>>(TYPES.Auth_ORMSharedVaultUserRepository),
+          container.get<MapperInterface<SharedVaultUser, TypeORMSharedVaultUser>>(
+            TYPES.Auth_SharedVaultUserPersistenceMapper,
+          ),
+        ),
+      )
 
     // Middleware
     container.bind<SessionMiddleware>(TYPES.Auth_SessionMiddleware).to(SessionMiddleware)
@@ -926,6 +951,18 @@ export class ContainerConfigLoader {
           container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
         ),
       )
+    container
+      .bind<AddSharedVaultUser>(TYPES.Auth_AddSharedVaultUser)
+      .toConstantValue(
+        new AddSharedVaultUser(container.get<SharedVaultUserRepositoryInterface>(TYPES.Auth_SharedVaultUserRepository)),
+      )
+    container
+      .bind<RemoveSharedVaultUser>(TYPES.Auth_RemoveSharedVaultUser)
+      .toConstantValue(
+        new RemoveSharedVaultUser(
+          container.get<SharedVaultUserRepositoryInterface>(TYPES.Auth_SharedVaultUserRepository),
+        ),
+      )
 
     // Controller
     container
@@ -1075,6 +1112,22 @@ export class ContainerConfigLoader {
           container.get<winston.Logger>(TYPES.Auth_Logger),
         ),
       )
+    container
+      .bind<UserAddedToSharedVaultEventHandler>(TYPES.Auth_UserAddedToSharedVaultEventHandler)
+      .toConstantValue(
+        new UserAddedToSharedVaultEventHandler(
+          container.get<AddSharedVaultUser>(TYPES.Auth_AddSharedVaultUser),
+          container.get<winston.Logger>(TYPES.Auth_Logger),
+        ),
+      )
+    container
+      .bind<UserRemovedFromSharedVaultEventHandler>(TYPES.Auth_UserRemovedFromSharedVaultEventHandler)
+      .toConstantValue(
+        new UserRemovedFromSharedVaultEventHandler(
+          container.get<RemoveSharedVaultUser>(TYPES.Auth_RemoveSharedVaultUser),
+          container.get<winston.Logger>(TYPES.Auth_Logger),
+        ),
+      )
 
     const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
       ['USER_REGISTERED', container.get(TYPES.Auth_UserRegisteredEventHandler)],
@@ -1107,6 +1160,8 @@ export class ContainerConfigLoader {
       ['EMAIL_SUBSCRIPTION_UNSUBSCRIBED', container.get(TYPES.Auth_EmailSubscriptionUnsubscribedEventHandler)],
       ['PAYMENTS_ACCOUNT_DELETED', container.get(TYPES.Auth_PaymentsAccountDeletedEventHandler)],
       ['TRANSITION_STATUS_UPDATED', container.get(TYPES.Auth_TransitionStatusUpdatedEventHandler)],
+      ['USER_ADDED_TO_SHARED_VAULT', container.get(TYPES.Auth_UserAddedToSharedVaultEventHandler)],
+      ['USER_REMOVED_FROM_SHARED_VAULT', container.get(TYPES.Auth_UserRemovedFromSharedVaultEventHandler)],
     ])
 
     if (isConfiguredForHomeServer) {

+ 2 - 0
packages/auth/src/Bootstrap/DataSource.ts

@@ -18,6 +18,7 @@ import { TypeORMEmergencyAccessInvitation } from '../Infra/TypeORM/TypeORMEmerge
 import { TypeORMSessionTrace } from '../Infra/TypeORM/TypeORMSessionTrace'
 import { Env } from './Env'
 import { SqliteConnectionOptions } from 'typeorm/driver/sqlite/SqliteConnectionOptions'
+import { TypeORMSharedVaultUser } from '../Infra/TypeORM/TypeORMSharedVaultUser'
 
 export class AppDataSource {
   private _dataSource: DataSource | undefined
@@ -64,6 +65,7 @@ export class AppDataSource {
         TypeORMAuthenticatorChallenge,
         TypeORMEmergencyAccessInvitation,
         TypeORMCacheEntry,
+        TypeORMSharedVaultUser,
       ],
       migrations: [`${__dirname}/../../migrations/${isConfiguredForMySQL ? 'mysql' : 'sqlite'}/*.js`],
       migrationsRun: true,

+ 7 - 0
packages/auth/src/Bootstrap/Types.ts

@@ -9,6 +9,7 @@ const TYPES = {
   Auth_AuthenticatorPersistenceMapper: Symbol.for('Auth_AuthenticatorPersistenceMapper'),
   Auth_AuthenticatorHttpMapper: Symbol.for('Auth_AuthenticatorHttpMapper'),
   Auth_CacheEntryPersistenceMapper: Symbol.for('Auth_CacheEntryPersistenceMapper'),
+  Auth_SharedVaultUserPersistenceMapper: Symbol.for('Auth_SharedVaultUserPersistenceMapper'),
   // Controller
   Auth_ControllerContainer: Symbol.for('Auth_ControllerContainer'),
   Auth_AuthController: Symbol.for('Auth_AuthController'),
@@ -36,6 +37,7 @@ const TYPES = {
   Auth_AuthenticatorChallengeRepository: Symbol.for('Auth_AuthenticatorChallengeRepository'),
   Auth_CacheEntryRepository: Symbol.for('Auth_CacheEntryRepository'),
   Auth_TransitionStatusRepository: Symbol.for('Auth_TransitionStatusRepository'),
+  Auth_SharedVaultUserRepository: Symbol.for('Auth_SharedVaultUserRepository'),
   // ORM
   Auth_ORMOfflineSettingRepository: Symbol.for('Auth_ORMOfflineSettingRepository'),
   Auth_ORMOfflineUserSubscriptionRepository: Symbol.for('Auth_ORMOfflineUserSubscriptionRepository'),
@@ -51,6 +53,7 @@ const TYPES = {
   Auth_ORMAuthenticatorRepository: Symbol.for('Auth_ORMAuthenticatorRepository'),
   Auth_ORMAuthenticatorChallengeRepository: Symbol.for('Auth_ORMAuthenticatorChallengeRepository'),
   Auth_ORMCacheEntryRepository: Symbol.for('Auth_ORMCacheEntryRepository'),
+  Auth_ORMSharedVaultUserRepository: Symbol.for('Auth_ORMSharedVaultUserRepository'),
   // Middleware
   Auth_RequiredCrossServiceTokenMiddleware: Symbol.for('Auth_RequiredCrossServiceTokenMiddleware'),
   Auth_OptionalCrossServiceTokenMiddleware: Symbol.for('Auth_OptionalCrossServiceTokenMiddleware'),
@@ -157,6 +160,8 @@ const TYPES = {
   Auth_UpdateStorageQuotaUsedForUser: Symbol.for('Auth_UpdateStorageQuotaUsedForUser'),
   Auth_UpdateTransitionStatus: Symbol.for('Auth_UpdateTransitionStatus'),
   Auth_GetTransitionStatus: Symbol.for('Auth_GetTransitionStatus'),
+  Auth_AddSharedVaultUser: Symbol.for('Auth_AddSharedVaultUser'),
+  Auth_RemoveSharedVaultUser: Symbol.for('Auth_RemoveSharedVaultUser'),
   // Handlers
   Auth_UserRegisteredEventHandler: Symbol.for('Auth_UserRegisteredEventHandler'),
   Auth_AccountDeletionRequestedEventHandler: Symbol.for('Auth_AccountDeletionRequestedEventHandler'),
@@ -186,6 +191,8 @@ const TYPES = {
   Auth_EmailSubscriptionUnsubscribedEventHandler: Symbol.for('Auth_EmailSubscriptionUnsubscribedEventHandler'),
   Auth_PaymentsAccountDeletedEventHandler: Symbol.for('Auth_PaymentsAccountDeletedEventHandler'),
   Auth_TransitionStatusUpdatedEventHandler: Symbol.for('Auth_TransitionStatusUpdatedEventHandler'),
+  Auth_UserAddedToSharedVaultEventHandler: Symbol.for('Auth_UserAddedToSharedVaultEventHandler'),
+  Auth_UserRemovedFromSharedVaultEventHandler: Symbol.for('Auth_UserRemovedFromSharedVaultEventHandler'),
   // Services
   Auth_DeviceDetector: Symbol.for('Auth_DeviceDetector'),
   Auth_SessionService: Symbol.for('Auth_SessionService'),

+ 24 - 0
packages/auth/src/Domain/Handler/UserAddedToSharedVaultEventHandler.ts

@@ -0,0 +1,24 @@
+import { DomainEventHandlerInterface, UserAddedToSharedVaultEvent } from '@standardnotes/domain-events'
+import { Logger } from 'winston'
+import { AddSharedVaultUser } from '../UseCase/AddSharedVaultUser/AddSharedVaultUser'
+
+export class UserAddedToSharedVaultEventHandler implements DomainEventHandlerInterface {
+  constructor(
+    private addSharedVaultUser: AddSharedVaultUser,
+    private logger: Logger,
+  ) {}
+
+  async handle(event: UserAddedToSharedVaultEvent): Promise<void> {
+    const result = await this.addSharedVaultUser.execute({
+      userUuid: event.payload.userUuid,
+      sharedVaultUuid: event.payload.sharedVaultUuid,
+      permission: event.payload.permission,
+      createdAt: event.payload.createdAt,
+      updatedAt: event.payload.updatedAt,
+    })
+
+    if (result.isFailed()) {
+      this.logger.error(`Failed to add user to shared vault: ${result.getError()}`)
+    }
+  }
+}

+ 22 - 0
packages/auth/src/Domain/Handler/UserRemovedFromSharedVaultEventHandler.ts

@@ -0,0 +1,22 @@
+import { DomainEventHandlerInterface, UserRemovedFromSharedVaultEvent } from '@standardnotes/domain-events'
+import { Logger } from 'winston'
+
+import { RemoveSharedVaultUser } from '../UseCase/RemoveSharedVaultUser/RemoveSharedVaultUser'
+
+export class UserRemovedFromSharedVaultEventHandler implements DomainEventHandlerInterface {
+  constructor(
+    private removeSharedVaultUser: RemoveSharedVaultUser,
+    private logger: Logger,
+  ) {}
+
+  async handle(event: UserRemovedFromSharedVaultEvent): Promise<void> {
+    const result = await this.removeSharedVaultUser.execute({
+      userUuid: event.payload.userUuid,
+      sharedVaultUuid: event.payload.sharedVaultUuid,
+    })
+
+    if (result.isFailed()) {
+      this.logger.error(`Failed to remove user from shared vault: ${result.getError()}`)
+    }
+  }
+}

+ 8 - 0
packages/auth/src/Domain/SharedVault/SharedVaultUserRepositoryInterface.ts

@@ -0,0 +1,8 @@
+import { SharedVaultUser, Uuid } from '@standardnotes/domain-core'
+
+export interface SharedVaultUserRepositoryInterface {
+  findByUserUuidAndSharedVaultUuid(dto: { userUuid: Uuid; sharedVaultUuid: Uuid }): Promise<SharedVaultUser | null>
+  findByUserUuid(userUuid: Uuid): Promise<SharedVaultUser[]>
+  save(sharedVaultUser: SharedVaultUser): Promise<void>
+  remove(sharedVault: SharedVaultUser): Promise<void>
+}

+ 106 - 0
packages/auth/src/Domain/UseCase/AddSharedVaultUser/AddSharedVaultUser.spec.ts

@@ -0,0 +1,106 @@
+import { Result, SharedVaultUser } from '@standardnotes/domain-core'
+import { SharedVaultUserRepositoryInterface } from '../../SharedVault/SharedVaultUserRepositoryInterface'
+import { AddSharedVaultUser } from './AddSharedVaultUser'
+
+describe('AddSharedVaultUser', () => {
+  let sharedVaultUserRepository: SharedVaultUserRepositoryInterface
+
+  const createUseCase = () => new AddSharedVaultUser(sharedVaultUserRepository)
+
+  beforeEach(() => {
+    sharedVaultUserRepository = {} as jest.Mocked<SharedVaultUserRepositoryInterface>
+    sharedVaultUserRepository.save = jest.fn()
+  })
+
+  it('should save shared vault user', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      userUuid: '00000000-0000-0000-0000-000000000000',
+      sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
+      permission: 'read',
+      createdAt: 1,
+      updatedAt: 2,
+    })
+
+    expect(result.isFailed()).toBeFalsy()
+    expect(sharedVaultUserRepository.save).toHaveBeenCalled()
+  })
+
+  it('should fail when user uuid is invalid', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      userUuid: 'invalid',
+      sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
+      permission: 'read',
+      createdAt: 1,
+      updatedAt: 2,
+    })
+
+    expect(result.isFailed()).toBeTruthy()
+    expect(sharedVaultUserRepository.save).not.toHaveBeenCalled()
+  })
+
+  it('should fail when shared vault uuid is invalid', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      userUuid: '00000000-0000-0000-0000-000000000000',
+      sharedVaultUuid: 'invalid',
+      permission: 'read',
+      createdAt: 1,
+      updatedAt: 2,
+    })
+
+    expect(result.isFailed()).toBeTruthy()
+    expect(sharedVaultUserRepository.save).not.toHaveBeenCalled()
+  })
+
+  it('should fail when permission is invalid', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      userUuid: '00000000-0000-0000-0000-000000000000',
+      sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
+      permission: 'invalid',
+      createdAt: 1,
+      updatedAt: 2,
+    })
+
+    expect(result.isFailed()).toBeTruthy()
+    expect(sharedVaultUserRepository.save).not.toHaveBeenCalled()
+  })
+
+  it('should fail when timestamps are invalid', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      userUuid: '00000000-0000-0000-0000-000000000000',
+      sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
+      permission: 'read',
+      createdAt: 'invalid' as unknown as number,
+      updatedAt: 1,
+    })
+
+    expect(result.isFailed()).toBeTruthy()
+    expect(sharedVaultUserRepository.save).not.toHaveBeenCalled()
+  })
+
+  it('should fail when shared vault user is invalid', async () => {
+    const mock = jest.spyOn(SharedVaultUser, 'create')
+    mock.mockReturnValue(Result.fail('Oops'))
+
+    const result = await createUseCase().execute({
+      userUuid: '00000000-0000-0000-0000-000000000000',
+      sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
+      permission: 'read',
+      createdAt: 2,
+      updatedAt: 1,
+    })
+
+    expect(result.isFailed()).toBeTruthy()
+
+    mock.mockRestore()
+  })
+})

+ 56 - 0
packages/auth/src/Domain/UseCase/AddSharedVaultUser/AddSharedVaultUser.ts

@@ -0,0 +1,56 @@
+import {
+  Result,
+  SharedVaultUser,
+  SharedVaultUserPermission,
+  Timestamps,
+  UseCaseInterface,
+  Uuid,
+} from '@standardnotes/domain-core'
+
+import { SharedVaultUserRepositoryInterface } from '../../SharedVault/SharedVaultUserRepositoryInterface'
+import { AddSharedVaultUserDTO } from './AddSharedVaultUserDTO'
+
+export class AddSharedVaultUser implements UseCaseInterface<void> {
+  constructor(private sharedVaultUserRepository: SharedVaultUserRepositoryInterface) {}
+
+  async execute(dto: AddSharedVaultUserDTO): Promise<Result<void>> {
+    const userUuidOrError = Uuid.create(dto.userUuid)
+    if (userUuidOrError.isFailed()) {
+      return Result.fail(userUuidOrError.getError())
+    }
+    const userUuid = userUuidOrError.getValue()
+
+    const sharedVaultUuidOrError = Uuid.create(dto.sharedVaultUuid)
+    if (sharedVaultUuidOrError.isFailed()) {
+      return Result.fail(sharedVaultUuidOrError.getError())
+    }
+    const sharedVaultUuid = sharedVaultUuidOrError.getValue()
+
+    const permissionOrError = SharedVaultUserPermission.create(dto.permission)
+    if (permissionOrError.isFailed()) {
+      return Result.fail(permissionOrError.getError())
+    }
+    const permission = permissionOrError.getValue()
+
+    const timestampsOrError = Timestamps.create(dto.createdAt, dto.updatedAt)
+    if (timestampsOrError.isFailed()) {
+      return Result.fail(timestampsOrError.getError())
+    }
+    const timestamps = timestampsOrError.getValue()
+
+    const sharedVaultUserOrError = SharedVaultUser.create({
+      userUuid,
+      sharedVaultUuid,
+      permission,
+      timestamps,
+    })
+    if (sharedVaultUserOrError.isFailed()) {
+      return Result.fail(sharedVaultUserOrError.getError())
+    }
+    const sharedVaultUser = sharedVaultUserOrError.getValue()
+
+    await this.sharedVaultUserRepository.save(sharedVaultUser)
+
+    return Result.ok()
+  }
+}

+ 7 - 0
packages/auth/src/Domain/UseCase/AddSharedVaultUser/AddSharedVaultUserDTO.ts

@@ -0,0 +1,7 @@
+export interface AddSharedVaultUserDTO {
+  sharedVaultUuid: string
+  userUuid: string
+  permission: string
+  createdAt: number
+  updatedAt: number
+}

+ 44 - 1
packages/auth/src/Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken.spec.ts

@@ -9,8 +9,9 @@ import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
 
 import { CreateCrossServiceToken } from './CreateCrossServiceToken'
 import { GetSetting } from '../GetSetting/GetSetting'
-import { Result } from '@standardnotes/domain-core'
+import { Result, SharedVaultUser, SharedVaultUserPermission, Timestamps, Uuid } from '@standardnotes/domain-core'
 import { TransitionStatusRepositoryInterface } from '../../Transition/TransitionStatusRepositoryInterface'
+import { SharedVaultUserRepositoryInterface } from '../../SharedVault/SharedVaultUserRepositoryInterface'
 
 describe('CreateCrossServiceToken', () => {
   let userProjector: ProjectorInterface<User>
@@ -20,6 +21,7 @@ describe('CreateCrossServiceToken', () => {
   let userRepository: UserRepositoryInterface
   let getSettingUseCase: GetSetting
   let transitionStatusRepository: TransitionStatusRepositoryInterface
+  let sharedVaultUserRepository: SharedVaultUserRepositoryInterface
   const jwtTTL = 60
 
   let session: Session
@@ -36,6 +38,7 @@ describe('CreateCrossServiceToken', () => {
       jwtTTL,
       getSettingUseCase,
       transitionStatusRepository,
+      sharedVaultUserRepository,
     )
 
   beforeEach(() => {
@@ -70,6 +73,16 @@ describe('CreateCrossServiceToken', () => {
 
     transitionStatusRepository = {} as jest.Mocked<TransitionStatusRepositoryInterface>
     transitionStatusRepository.getStatus = jest.fn().mockReturnValue('TO-DO')
+
+    sharedVaultUserRepository = {} as jest.Mocked<SharedVaultUserRepositoryInterface>
+    sharedVaultUserRepository.findByUserUuid = jest.fn().mockReturnValue([
+      SharedVaultUser.create({
+        permission: SharedVaultUserPermission.create('read').getValue(),
+        sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+        timestamps: Timestamps.create(123456789, 123456789).getValue(),
+        userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+      }).getValue(),
+    ])
   })
 
   it('should create a cross service token for user', async () => {
@@ -86,6 +99,12 @@ describe('CreateCrossServiceToken', () => {
             uuid: '1-3-4',
           },
         ],
+        belongs_to_shared_vaults: [
+          {
+            shared_vault_uuid: '00000000-0000-0000-0000-000000000000',
+            permission: 'read',
+          },
+        ],
         session: {
           test: 'test',
         },
@@ -115,6 +134,12 @@ describe('CreateCrossServiceToken', () => {
             uuid: '1-3-4',
           },
         ],
+        belongs_to_shared_vaults: [
+          {
+            shared_vault_uuid: '00000000-0000-0000-0000-000000000000',
+            permission: 'read',
+          },
+        ],
         session: {
           test: 'test',
         },
@@ -141,6 +166,12 @@ describe('CreateCrossServiceToken', () => {
             uuid: '1-3-4',
           },
         ],
+        belongs_to_shared_vaults: [
+          {
+            shared_vault_uuid: '00000000-0000-0000-0000-000000000000',
+            permission: 'read',
+          },
+        ],
         user: {
           email: 'test@test.te',
           uuid: '00000000-0000-0000-0000-000000000000',
@@ -164,6 +195,12 @@ describe('CreateCrossServiceToken', () => {
             uuid: '1-3-4',
           },
         ],
+        belongs_to_shared_vaults: [
+          {
+            shared_vault_uuid: '00000000-0000-0000-0000-000000000000',
+            permission: 'read',
+          },
+        ],
         user: {
           email: 'test@test.te',
           uuid: '00000000-0000-0000-0000-000000000000',
@@ -208,6 +245,12 @@ describe('CreateCrossServiceToken', () => {
               uuid: '1-3-4',
             },
           ],
+          belongs_to_shared_vaults: [
+            {
+              shared_vault_uuid: '00000000-0000-0000-0000-000000000000',
+              permission: 'read',
+            },
+          ],
           session: {
             test: 'test',
           },

+ 10 - 0
packages/auth/src/Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken.ts

@@ -13,6 +13,7 @@ import { CreateCrossServiceTokenDTO } from './CreateCrossServiceTokenDTO'
 import { GetSetting } from '../GetSetting/GetSetting'
 import { SettingName } from '@standardnotes/settings'
 import { TransitionStatusRepositoryInterface } from '../../Transition/TransitionStatusRepositoryInterface'
+import { SharedVaultUserRepositoryInterface } from '../../SharedVault/SharedVaultUserRepositoryInterface'
 
 @injectable()
 export class CreateCrossServiceToken implements UseCaseInterface<string> {
@@ -27,6 +28,7 @@ export class CreateCrossServiceToken implements UseCaseInterface<string> {
     private getSettingUseCase: GetSetting,
     @inject(TYPES.Auth_TransitionStatusRepository)
     private transitionStatusRepository: TransitionStatusRepositoryInterface,
+    @inject(TYPES.Auth_SharedVaultUserRepository) private sharedVaultUserRepository: SharedVaultUserRepositoryInterface,
   ) {}
 
   async execute(dto: CreateCrossServiceTokenDTO): Promise<Result<string>> {
@@ -49,11 +51,19 @@ export class CreateCrossServiceToken implements UseCaseInterface<string> {
 
     const roles = await user.roles
 
+    const sharedVaultAssociations = await this.sharedVaultUserRepository.findByUserUuid(
+      Uuid.create(user.uuid).getValue(),
+    )
+
     const authTokenData: CrossServiceTokenData = {
       user: this.projectUser(user),
       roles: this.projectRoles(roles),
       shared_vault_owner_context: undefined,
       ongoing_transition: transitionStatus === 'STARTED',
+      belongs_to_shared_vaults: sharedVaultAssociations.map((association) => ({
+        shared_vault_uuid: association.props.sharedVaultUuid.value,
+        permission: association.props.permission.value,
+      })),
     }
 
     if (dto.sharedVaultOwnerContext !== undefined) {

+ 68 - 0
packages/auth/src/Domain/UseCase/RemoveSharedVaultUser/RemoveSharedVaultUser.spec.ts

@@ -0,0 +1,68 @@
+import { SharedVaultUser } from '@standardnotes/domain-core'
+
+import { SharedVaultUserRepositoryInterface } from '../../SharedVault/SharedVaultUserRepositoryInterface'
+import { RemoveSharedVaultUser } from './RemoveSharedVaultUser'
+
+describe('RemoveSharedVaultUser', () => {
+  let sharedVaultUserRepository: SharedVaultUserRepositoryInterface
+
+  const createUseCase = () => new RemoveSharedVaultUser(sharedVaultUserRepository)
+
+  beforeEach(() => {
+    sharedVaultUserRepository = {} as jest.Mocked<SharedVaultUserRepositoryInterface>
+    sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest
+      .fn()
+      .mockReturnValue({} as jest.Mocked<SharedVaultUser>)
+    sharedVaultUserRepository.remove = jest.fn()
+  })
+
+  it('should remove shared vault user', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      userUuid: '00000000-0000-0000-0000-000000000000',
+      sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
+    })
+
+    expect(result.isFailed()).toBeFalsy()
+    expect(sharedVaultUserRepository.remove).toHaveBeenCalled()
+  })
+
+  it('should fail when user uuid is invalid', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      userUuid: 'invalid',
+      sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
+    })
+
+    expect(result.isFailed()).toBeTruthy()
+    expect(sharedVaultUserRepository.remove).not.toHaveBeenCalled()
+  })
+
+  it('should fail when shared vault uuid is invalid', async () => {
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      userUuid: '00000000-0000-0000-0000-000000000000',
+      sharedVaultUuid: 'invalid',
+    })
+
+    expect(result.isFailed()).toBeTruthy()
+    expect(sharedVaultUserRepository.remove).not.toHaveBeenCalled()
+  })
+
+  it('should fail when shared vault user is not found', async () => {
+    sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid = jest.fn().mockReturnValue(null)
+
+    const useCase = createUseCase()
+
+    const result = await useCase.execute({
+      userUuid: '00000000-0000-0000-0000-000000000000',
+      sharedVaultUuid: '00000000-0000-0000-0000-000000000000',
+    })
+
+    expect(result.isFailed()).toBeTruthy()
+    expect(sharedVaultUserRepository.remove).not.toHaveBeenCalled()
+  })
+})

+ 34 - 0
packages/auth/src/Domain/UseCase/RemoveSharedVaultUser/RemoveSharedVaultUser.ts

@@ -0,0 +1,34 @@
+import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
+
+import { SharedVaultUserRepositoryInterface } from '../../SharedVault/SharedVaultUserRepositoryInterface'
+import { RemoveSharedVaultUserDTO } from './RemoveSharedVaultUserDTO'
+
+export class RemoveSharedVaultUser implements UseCaseInterface<void> {
+  constructor(private sharedVaultUserRepository: SharedVaultUserRepositoryInterface) {}
+
+  async execute(dto: RemoveSharedVaultUserDTO): Promise<Result<void>> {
+    const userUuidOrError = Uuid.create(dto.userUuid)
+    if (userUuidOrError.isFailed()) {
+      return Result.fail(userUuidOrError.getError())
+    }
+    const userUuid = userUuidOrError.getValue()
+
+    const sharedVaultUuidOrError = Uuid.create(dto.sharedVaultUuid)
+    if (sharedVaultUuidOrError.isFailed()) {
+      return Result.fail(sharedVaultUuidOrError.getError())
+    }
+    const sharedVaultUuid = sharedVaultUuidOrError.getValue()
+
+    const sharedVaultUser = await this.sharedVaultUserRepository.findByUserUuidAndSharedVaultUuid({
+      userUuid,
+      sharedVaultUuid,
+    })
+    if (!sharedVaultUser) {
+      return Result.fail('Shared vault user not found')
+    }
+
+    await this.sharedVaultUserRepository.remove(sharedVaultUser)
+
+    return Result.ok()
+  }
+}

+ 4 - 0
packages/auth/src/Domain/UseCase/RemoveSharedVaultUser/RemoveSharedVaultUserDTO.ts

@@ -0,0 +1,4 @@
+export interface RemoveSharedVaultUserDTO {
+  sharedVaultUuid: string
+  userUuid: string
+}

+ 40 - 0
packages/auth/src/Infra/TypeORM/TypeORMSharedVaultUser.ts

@@ -0,0 +1,40 @@
+import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'
+
+@Entity({ name: 'auth_shared_vault_users' })
+export class TypeORMSharedVaultUser {
+  @PrimaryGeneratedColumn('uuid')
+  declare uuid: string
+
+  @Column({
+    name: 'shared_vault_uuid',
+    length: 36,
+  })
+  @Index('shared_vault_uuid_on_auth_shared_vault_users')
+  declare sharedVaultUuid: string
+
+  @Column({
+    name: 'user_uuid',
+    length: 36,
+  })
+  @Index('user_uuid_on_auth_shared_vault_users')
+  declare userUuid: string
+
+  @Column({
+    name: 'permission',
+    type: 'varchar',
+    length: 24,
+  })
+  declare permission: string
+
+  @Column({
+    name: 'created_at_timestamp',
+    type: 'bigint',
+  })
+  declare createdAtTimestamp: number
+
+  @Column({
+    name: 'updated_at_timestamp',
+    type: 'bigint',
+  })
+  declare updatedAtTimestamp: number
+}

+ 54 - 0
packages/auth/src/Infra/TypeORM/TypeORMSharedVaultUserRepository.ts

@@ -0,0 +1,54 @@
+import { Repository } from 'typeorm'
+import { MapperInterface, SharedVaultUser, Uuid } from '@standardnotes/domain-core'
+
+import { SharedVaultUserRepositoryInterface } from '../../Domain/SharedVault/SharedVaultUserRepositoryInterface'
+import { TypeORMSharedVaultUser } from './TypeORMSharedVaultUser'
+
+export class TypeORMSharedVaultUserRepository implements SharedVaultUserRepositoryInterface {
+  constructor(
+    private ormRepository: Repository<TypeORMSharedVaultUser>,
+    private mapper: MapperInterface<SharedVaultUser, TypeORMSharedVaultUser>,
+  ) {}
+
+  async findByUserUuid(userUuid: Uuid): Promise<SharedVaultUser[]> {
+    const persistence = await this.ormRepository
+      .createQueryBuilder('shared_vault_user')
+      .where('shared_vault_user.user_uuid = :userUuid', {
+        userUuid: userUuid.value,
+      })
+      .getMany()
+
+    return persistence.map((p) => this.mapper.toDomain(p))
+  }
+
+  async findByUserUuidAndSharedVaultUuid(dto: {
+    userUuid: Uuid
+    sharedVaultUuid: Uuid
+  }): Promise<SharedVaultUser | null> {
+    const persistence = await this.ormRepository
+      .createQueryBuilder('shared_vault_user')
+      .where('shared_vault_user.user_uuid = :userUuid', {
+        userUuid: dto.userUuid.value,
+      })
+      .andWhere('shared_vault_user.shared_vault_uuid = :sharedVaultUuid', {
+        sharedVaultUuid: dto.sharedVaultUuid.value,
+      })
+      .getOne()
+
+    if (persistence === null) {
+      return null
+    }
+
+    return this.mapper.toDomain(persistence)
+  }
+
+  async save(sharedVaultUser: SharedVaultUser): Promise<void> {
+    const persistence = this.mapper.toProjection(sharedVaultUser)
+
+    await this.ormRepository.save(persistence)
+  }
+
+  async remove(sharedVaultUser: SharedVaultUser): Promise<void> {
+    await this.ormRepository.remove(this.mapper.toProjection(sharedVaultUser))
+  }
+}

+ 67 - 0
packages/auth/src/Mapping/SharedVaultUserPersistenceMapper.ts

@@ -0,0 +1,67 @@
+import {
+  Timestamps,
+  MapperInterface,
+  UniqueEntityId,
+  Uuid,
+  SharedVaultUserPermission,
+  SharedVaultUser,
+} from '@standardnotes/domain-core'
+
+import { TypeORMSharedVaultUser } from '../Infra/TypeORM/TypeORMSharedVaultUser'
+
+export class SharedVaultUserPersistenceMapper implements MapperInterface<SharedVaultUser, TypeORMSharedVaultUser> {
+  toDomain(projection: TypeORMSharedVaultUser): SharedVaultUser {
+    const userUuidOrError = Uuid.create(projection.userUuid)
+    if (userUuidOrError.isFailed()) {
+      throw new Error(`Failed to create shared vault user from projection: ${userUuidOrError.getError()}`)
+    }
+    const userUuid = userUuidOrError.getValue()
+
+    const sharedVaultUuidOrError = Uuid.create(projection.sharedVaultUuid)
+    if (sharedVaultUuidOrError.isFailed()) {
+      throw new Error(`Failed to create shared vault user from projection: ${sharedVaultUuidOrError.getError()}`)
+    }
+    const sharedVaultUuid = sharedVaultUuidOrError.getValue()
+
+    const timestampsOrError = Timestamps.create(projection.createdAtTimestamp, projection.updatedAtTimestamp)
+    if (timestampsOrError.isFailed()) {
+      throw new Error(`Failed to create shared vault user from projection: ${timestampsOrError.getError()}`)
+    }
+    const timestamps = timestampsOrError.getValue()
+
+    const permissionOrError = SharedVaultUserPermission.create(projection.permission)
+    if (permissionOrError.isFailed()) {
+      throw new Error(`Failed to create shared vault user from projection: ${permissionOrError.getError()}`)
+    }
+    const permission = permissionOrError.getValue()
+
+    const sharedVaultUserOrError = SharedVaultUser.create(
+      {
+        userUuid,
+        sharedVaultUuid,
+        permission,
+        timestamps,
+      },
+      new UniqueEntityId(projection.uuid),
+    )
+    if (sharedVaultUserOrError.isFailed()) {
+      throw new Error(`Failed to create shared vault user from projection: ${sharedVaultUserOrError.getError()}`)
+    }
+    const sharedVaultUser = sharedVaultUserOrError.getValue()
+
+    return sharedVaultUser
+  }
+
+  toProjection(domain: SharedVaultUser): TypeORMSharedVaultUser {
+    const typeorm = new TypeORMSharedVaultUser()
+
+    typeorm.uuid = domain.id.toString()
+    typeorm.sharedVaultUuid = domain.props.sharedVaultUuid.value
+    typeorm.userUuid = domain.props.userUuid.value
+    typeorm.permission = domain.props.permission.value
+    typeorm.createdAtTimestamp = domain.props.timestamps.createdAt
+    typeorm.updatedAtTimestamp = domain.props.timestamps.updatedAt
+
+    return typeorm
+  }
+}

+ 23 - 0
packages/domain-core/src/Domain/Common/Timestamps.spec.ts

@@ -0,0 +1,23 @@
+import { Timestamps } from './Timestamps'
+
+describe('Timestamps', () => {
+  it('should create a value object', () => {
+    const valueOrError = Timestamps.create(123, 234)
+
+    expect(valueOrError.isFailed()).toBeFalsy()
+    expect(valueOrError.getValue().createdAt).toEqual(123)
+    expect(valueOrError.getValue().updatedAt).toEqual(234)
+  })
+
+  it('should not create an invalid value object', () => {
+    const valueOrError = Timestamps.create('' as unknown as number, 123)
+
+    expect(valueOrError.isFailed()).toBeTruthy()
+  })
+
+  it('should not create an invalid value object', () => {
+    const valueOrError = Timestamps.create(123, '' as unknown as number)
+
+    expect(valueOrError.isFailed()).toBeTruthy()
+  })
+})

+ 2 - 2
packages/domain-core/src/Domain/Common/Timestamps.ts

@@ -16,12 +16,12 @@ export class Timestamps extends ValueObject<TimestampsProps> {
   }
 
   static create(createdAt: number, updatedAt: number): Result<Timestamps> {
-    if (isNaN(createdAt)) {
+    if (isNaN(createdAt) || typeof createdAt !== 'number') {
       return Result.fail<Timestamps>(
         `Could not create Timestamps. Creation date should be a number, given: ${createdAt}`,
       )
     }
-    if (isNaN(updatedAt)) {
+    if (isNaN(updatedAt) || typeof updatedAt !== 'number') {
       return Result.fail<Timestamps>(`Could not create Timestamps. Update date should be a number, given: ${updatedAt}`)
     }
 

+ 3 - 2
packages/syncing-server/src/Domain/SharedVault/User/SharedVaultUser.spec.ts → packages/domain-core/src/Domain/SharedVault/SharedVaultUser.spec.ts

@@ -1,6 +1,7 @@
-import { SharedVaultUserPermission, Timestamps, Uuid } from '@standardnotes/domain-core'
-
+import { SharedVaultUserPermission } from './SharedVaultUserPermission'
 import { SharedVaultUser } from './SharedVaultUser'
+import { Uuid } from '../Common/Uuid'
+import { Timestamps } from '../Common/Timestamps'
 
 describe('SharedVaultUser', () => {
   it('should create an entity', () => {

+ 3 - 2
packages/syncing-server/src/Domain/SharedVault/User/SharedVaultUser.ts → packages/domain-core/src/Domain/SharedVault/SharedVaultUser.ts

@@ -1,5 +1,6 @@
-import { Entity, Result, UniqueEntityId } from '@standardnotes/domain-core'
-
+import { Entity } from '../Core/Entity'
+import { Result } from '../Core/Result'
+import { UniqueEntityId } from '../Core/UniqueEntityId'
 import { SharedVaultUserProps } from './SharedVaultUserProps'
 
 export class SharedVaultUser extends Entity<SharedVaultUserProps> {

+ 10 - 0
packages/domain-core/src/Domain/SharedVault/SharedVaultUserProps.ts

@@ -0,0 +1,10 @@
+import { Timestamps } from '../Common/Timestamps'
+import { Uuid } from '../Common/Uuid'
+import { SharedVaultUserPermission } from './SharedVaultUserPermission'
+
+export interface SharedVaultUserProps {
+  sharedVaultUuid: Uuid
+  userUuid: Uuid
+  permission: SharedVaultUserPermission
+  timestamps: Timestamps
+}

+ 2 - 0
packages/domain-core/src/Domain/index.ts

@@ -59,8 +59,10 @@ export * from './Service/ServiceIdentifier'
 export * from './Service/ServiceIdentifierProps'
 export * from './Service/ServiceInterface'
 
+export * from './SharedVault/SharedVaultUser'
 export * from './SharedVault/SharedVaultUserPermission'
 export * from './SharedVault/SharedVaultUserPermissionProps'
+export * from './SharedVault/SharedVaultUserProps'
 
 export * from './Subscription/SubscriptionPlanName'
 export * from './Subscription/SubscriptionPlanNameProps'

+ 7 - 0
packages/domain-events/src/Domain/Event/UserAddedToSharedVaultEvent.ts

@@ -0,0 +1,7 @@
+import { DomainEventInterface } from './DomainEventInterface'
+import { UserAddedToSharedVaultEventPayload } from './UserAddedToSharedVaultEventPayload'
+
+export interface UserAddedToSharedVaultEvent extends DomainEventInterface {
+  type: 'USER_ADDED_TO_SHARED_VAULT'
+  payload: UserAddedToSharedVaultEventPayload
+}

+ 7 - 0
packages/domain-events/src/Domain/Event/UserAddedToSharedVaultEventPayload.ts

@@ -0,0 +1,7 @@
+export interface UserAddedToSharedVaultEventPayload {
+  sharedVaultUuid: string
+  userUuid: string
+  permission: string
+  createdAt: number
+  updatedAt: number
+}

+ 7 - 0
packages/domain-events/src/Domain/Event/UserRemovedFromSharedVaultEvent.ts

@@ -0,0 +1,7 @@
+import { DomainEventInterface } from './DomainEventInterface'
+import { UserRemovedFromSharedVaultEventPayload } from './UserRemovedFromSharedVaultEventPayload'
+
+export interface UserRemovedFromSharedVaultEvent extends DomainEventInterface {
+  type: 'USER_REMOVED_FROM_SHARED_VAULT'
+  payload: UserRemovedFromSharedVaultEventPayload
+}

+ 4 - 0
packages/domain-events/src/Domain/Event/UserRemovedFromSharedVaultEventPayload.ts

@@ -0,0 +1,4 @@
+export interface UserRemovedFromSharedVaultEventPayload {
+  sharedVaultUuid: string
+  userUuid: string
+}

+ 4 - 0
packages/domain-events/src/Domain/index.ts

@@ -96,6 +96,8 @@ export * from './Event/SubscriptionSyncRequestedEvent'
 export * from './Event/SubscriptionSyncRequestedEventPayload'
 export * from './Event/TransitionStatusUpdatedEvent'
 export * from './Event/TransitionStatusUpdatedEventPayload'
+export * from './Event/UserAddedToSharedVaultEvent'
+export * from './Event/UserAddedToSharedVaultEventPayload'
 export * from './Event/UserDisabledSessionUserAgentLoggingEvent'
 export * from './Event/UserDisabledSessionUserAgentLoggingEventPayload'
 export * from './Event/UserEmailChangedEvent'
@@ -104,6 +106,8 @@ export * from './Event/UserInvitedToSharedVaultEvent'
 export * from './Event/UserInvitedToSharedVaultEventPayload'
 export * from './Event/UserRegisteredEvent'
 export * from './Event/UserRegisteredEventPayload'
+export * from './Event/UserRemovedFromSharedVaultEvent'
+export * from './Event/UserRemovedFromSharedVaultEventPayload'
 export * from './Event/UserRolesChangedEvent'
 export * from './Event/UserRolesChangedEventPayload'
 export * from './Event/WebSocketMessageRequestedEvent'

+ 41 - 0
packages/revisions/migrations/mysql-legacy/1669113322388-init.ts

@@ -0,0 +1,41 @@
+import { MigrationInterface, QueryRunner } from 'typeorm'
+
+export class init1669113322388 implements MigrationInterface {
+  name = 'init1669113322388'
+
+  public async up(queryRunner: QueryRunner): Promise<void> {
+    await this.syncSchemaBetweenLegacyRevisions(queryRunner)
+
+    await queryRunner.query(
+      'CREATE TABLE IF NOT EXISTS `revisions` (`uuid` varchar(36) NOT NULL, `item_uuid` varchar(36) NOT NULL, `user_uuid` varchar(36) NOT NULL, `content` mediumtext NULL, `content_type` varchar(255) NULL, `items_key_id` varchar(255) NULL, `enc_item_key` text NULL, `auth_hash` varchar(255) NULL, `creation_date` date NULL, `created_at` datetime(6) NULL, `updated_at` datetime(6) NULL, INDEX `item_uuid` (`item_uuid`), INDEX `user_uuid` (`user_uuid`), INDEX `creation_date` (`creation_date`), INDEX `created_at` (`created_at`), PRIMARY KEY (`uuid`)) ENGINE=InnoDB',
+    )
+  }
+
+  public async down(queryRunner: QueryRunner): Promise<void> {
+    await queryRunner.query('DROP INDEX `created_at` ON `revisions`')
+    await queryRunner.query('DROP INDEX `creation_date` ON `revisions`')
+    await queryRunner.query('DROP INDEX `user_uuid` ON `revisions`')
+    await queryRunner.query('DROP INDEX `item_uuid` ON `revisions`')
+    await queryRunner.query('DROP TABLE `revisions`')
+  }
+
+  private async syncSchemaBetweenLegacyRevisions(queryRunner: QueryRunner): Promise<void> {
+    const revisionsTableExistsQueryResult = await queryRunner.manager.query(
+      'SELECT COUNT(*) as count FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = "revisions"',
+    )
+    const revisionsTableExists = revisionsTableExistsQueryResult[0].count === 1
+    if (!revisionsTableExists) {
+      return
+    }
+
+    const revisionsTableHasUserUuidColumnQueryResult = await queryRunner.manager.query(
+      'SELECT COUNT(*) as count FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = "revisions" AND column_name = "user_uuid"',
+    )
+    const revisionsTableHasUserUuidColumn = revisionsTableHasUserUuidColumnQueryResult[0].count === 1
+    if (revisionsTableHasUserUuidColumn) {
+      return
+    }
+
+    await queryRunner.query('ALTER TABLE `revisions` ADD COLUMN `user_uuid` varchar(36) NULL')
+  }
+}

+ 28 - 0
packages/revisions/migrations/mysql-legacy/1669636497932-remove-date-indexes.ts

@@ -0,0 +1,28 @@
+import { MigrationInterface, QueryRunner } from 'typeorm'
+
+export class removeDateIndexes1669636497932 implements MigrationInterface {
+  name = 'removeDateIndexes1669636497932'
+
+  public async up(queryRunner: QueryRunner): Promise<void> {
+    const indexRevisionsOnCreatedAt = await queryRunner.manager.query(
+      'SHOW INDEX FROM `revisions` where `key_name` = "created_at"',
+    )
+    const indexRevisionsOnCreatedAtExist = indexRevisionsOnCreatedAt && indexRevisionsOnCreatedAt.length > 0
+    if (indexRevisionsOnCreatedAtExist) {
+      await queryRunner.query('DROP INDEX `created_at` ON `revisions`')
+    }
+
+    const indexRevisionsOnCreationDate = await queryRunner.manager.query(
+      'SHOW INDEX FROM `revisions` where `key_name` = "creation_date"',
+    )
+    const indexRevisionsOnCreationDateAtExist = indexRevisionsOnCreationDate && indexRevisionsOnCreationDate.length > 0
+    if (indexRevisionsOnCreationDateAtExist) {
+      await queryRunner.query('DROP INDEX `creation_date` ON `revisions`')
+    }
+  }
+
+  public async down(queryRunner: QueryRunner): Promise<void> {
+    await queryRunner.query('CREATE INDEX `creation_date` ON `revisions` (`creation_date`)')
+    await queryRunner.query('CREATE INDEX `created_at` ON `revisions` (`created_at`)')
+  }
+}

+ 13 - 0
packages/revisions/migrations/mysql-legacy/1669735585016-make-user-uuid-nullable.ts

@@ -0,0 +1,13 @@
+import { MigrationInterface, QueryRunner } from 'typeorm'
+
+export class makeUserUuidNullable1669735585016 implements MigrationInterface {
+  name = 'makeUserUuidNullable1669735585016'
+
+  public async up(queryRunner: QueryRunner): Promise<void> {
+    await queryRunner.query('ALTER TABLE `revisions` CHANGE `user_uuid` `user_uuid` varchar(36) NULL')
+  }
+
+  public async down(queryRunner: QueryRunner): Promise<void> {
+    await queryRunner.query('ALTER TABLE `revisions` CHANGE `user_uuid` `user_uuid` varchar(36) NOT NULL')
+  }
+}

+ 17 - 0
packages/revisions/migrations/mysql/1693915383950-add-shared-vault-information.ts

@@ -0,0 +1,17 @@
+import { MigrationInterface, QueryRunner } from 'typeorm'
+
+export class AddSharedVaultInformation1693915383950 implements MigrationInterface {
+  public async up(queryRunner: QueryRunner): Promise<void> {
+    await queryRunner.query('ALTER TABLE `revisions` ADD `edited_by` varchar(36) NULL')
+    await queryRunner.query('ALTER TABLE `revisions` ADD `shared_vault_uuid` varchar(36) NULL')
+    await queryRunner.query('ALTER TABLE `revisions` ADD `key_system_identifier` varchar(36) NULL')
+    await queryRunner.query('CREATE INDEX `index_revisions_on_shared_vault_uuid` ON `revisions` (`shared_vault_uuid`)')
+  }
+
+  public async down(queryRunner: QueryRunner): Promise<void> {
+    await queryRunner.query('DROP INDEX `index_revisions_on_shared_vault_uuid` ON `revisions`')
+    await queryRunner.query('ALTER TABLE `revisions` DROP COLUMN `key_system_identifier`')
+    await queryRunner.query('ALTER TABLE `revisions` DROP COLUMN `shared_vault_uuid`')
+    await queryRunner.query('ALTER TABLE `revisions` DROP COLUMN `last_edited_by`')
+  }
+}

+ 37 - 0
packages/revisions/migrations/sqlite/1693915775491-add-shared-vault-information.ts

@@ -0,0 +1,37 @@
+import { MigrationInterface, QueryRunner } from 'typeorm'
+
+export class AddSharedVaultInformation1693915775491 implements MigrationInterface {
+  name = 'AddSharedVaultInformation1693915775491'
+
+  public async up(queryRunner: QueryRunner): Promise<void> {
+    await queryRunner.query('DROP INDEX "user_uuid"')
+    await queryRunner.query('DROP INDEX "item_uuid"')
+    await queryRunner.query(
+      'CREATE TABLE "temporary_revisions" ("uuid" varchar PRIMARY KEY NOT NULL, "item_uuid" varchar(36) NOT NULL, "user_uuid" varchar(36), "content" text, "content_type" varchar(255), "items_key_id" varchar(255), "enc_item_key" text, "auth_hash" varchar(255), "creation_date" date, "created_at" datetime(6), "updated_at" datetime(6), "edited_by" varchar(36), "shared_vault_uuid" varchar(36), "key_system_identifier" varchar(36))',
+    )
+    await queryRunner.query(
+      'INSERT INTO "temporary_revisions"("uuid", "item_uuid", "user_uuid", "content", "content_type", "items_key_id", "enc_item_key", "auth_hash", "creation_date", "created_at", "updated_at") SELECT "uuid", "item_uuid", "user_uuid", "content", "content_type", "items_key_id", "enc_item_key", "auth_hash", "creation_date", "created_at", "updated_at" FROM "revisions"',
+    )
+    await queryRunner.query('DROP TABLE "revisions"')
+    await queryRunner.query('ALTER TABLE "temporary_revisions" RENAME TO "revisions"')
+    await queryRunner.query('CREATE INDEX "user_uuid" ON "revisions" ("user_uuid") ')
+    await queryRunner.query('CREATE INDEX "item_uuid" ON "revisions" ("item_uuid") ')
+    await queryRunner.query('CREATE INDEX "index_revisions_on_shared_vault_uuid" ON "revisions" ("shared_vault_uuid") ')
+  }
+
+  public async down(queryRunner: QueryRunner): Promise<void> {
+    await queryRunner.query('DROP INDEX "index_revisions_on_shared_vault_uuid"')
+    await queryRunner.query('DROP INDEX "item_uuid"')
+    await queryRunner.query('DROP INDEX "user_uuid"')
+    await queryRunner.query('ALTER TABLE "revisions" RENAME TO "temporary_revisions"')
+    await queryRunner.query(
+      'CREATE TABLE "revisions" ("uuid" varchar PRIMARY KEY NOT NULL, "item_uuid" varchar(36) NOT NULL, "user_uuid" varchar(36), "content" text, "content_type" varchar(255), "items_key_id" varchar(255), "enc_item_key" text, "auth_hash" varchar(255), "creation_date" date, "created_at" datetime(6), "updated_at" datetime(6))',
+    )
+    await queryRunner.query(
+      'INSERT INTO "revisions"("uuid", "item_uuid", "user_uuid", "content", "content_type", "items_key_id", "enc_item_key", "auth_hash", "creation_date", "created_at", "updated_at") SELECT "uuid", "item_uuid", "user_uuid", "content", "content_type", "items_key_id", "enc_item_key", "auth_hash", "creation_date", "created_at", "updated_at" FROM "temporary_revisions"',
+    )
+    await queryRunner.query('DROP TABLE "temporary_revisions"')
+    await queryRunner.query('CREATE INDEX "item_uuid" ON "revisions" ("item_uuid") ')
+    await queryRunner.query('CREATE INDEX "user_uuid" ON "revisions" ("user_uuid") ')
+  }
+}

+ 41 - 13
packages/revisions/src/Bootstrap/Container.ts

@@ -7,8 +7,8 @@ import { SNSClient, SNSClientConfig } from '@aws-sdk/client-sns'
 import { Revision } from '../Domain/Revision/Revision'
 import { RevisionMetadata } from '../Domain/Revision/RevisionMetadata'
 import { RevisionRepositoryInterface } from '../Domain/Revision/RevisionRepositoryInterface'
-import { SQLRevisionRepository } from '../Infra/TypeORM/SQL/SQLRevisionRepository'
-import { SQLRevision } from '../Infra/TypeORM/SQL/SQLRevision'
+import { SQLLegacyRevisionRepository } from '../Infra/TypeORM/SQL/SQLLegacyRevisionRepository'
+import { SQLLegacyRevision } from '../Infra/TypeORM/SQL/SQLLegacyRevision'
 import { AppDataSource } from './DataSource'
 import { Env } from './Env'
 import TYPES from './Types'
@@ -48,8 +48,8 @@ import { BaseRevisionsController } from '../Infra/InversifyExpress/Base/BaseRevi
 import { Transform } from 'stream'
 import { MongoDBRevision } from '../Infra/TypeORM/MongoDB/MongoDBRevision'
 import { MongoDBRevisionRepository } from '../Infra/TypeORM/MongoDB/MongoDBRevisionRepository'
-import { SQLRevisionMetadataPersistenceMapper } from '../Mapping/Persistence/SQL/SQLRevisionMetadataPersistenceMapper'
-import { SQLRevisionPersistenceMapper } from '../Mapping/Persistence/SQL/SQLRevisionPersistenceMapper'
+import { SQLLegacyRevisionMetadataPersistenceMapper } from '../Mapping/Persistence/SQL/SQLLegacyRevisionMetadataPersistenceMapper'
+import { SQLLegacyRevisionPersistenceMapper } from '../Mapping/Persistence/SQL/SQLLegacyRevisionPersistenceMapper'
 import { MongoDBRevisionMetadataPersistenceMapper } from '../Mapping/Persistence/MongoDB/MongoDBRevisionMetadataPersistenceMapper'
 import { MongoDBRevisionPersistenceMapper } from '../Mapping/Persistence/MongoDB/MongoDBRevisionPersistenceMapper'
 import { RevisionHttpMapper } from '../Mapping/Http/RevisionHttpMapper'
@@ -62,6 +62,10 @@ import { DomainEventFactoryInterface } from '../Domain/Event/DomainEventFactoryI
 import { DomainEventFactory } from '../Domain/Event/DomainEventFactory'
 import { TransitionStatusUpdatedEventHandler } from '../Domain/Handler/TransitionStatusUpdatedEventHandler'
 import { TriggerTransitionFromPrimaryToSecondaryDatabaseForUser } from '../Domain/UseCase/Transition/TriggerTransitionFromPrimaryToSecondaryDatabaseForUser/TriggerTransitionFromPrimaryToSecondaryDatabaseForUser'
+import { SQLRevision } from '../Infra/TypeORM/SQL/SQLRevision'
+import { SQLRevisionRepository } from '../Infra/TypeORM/SQL/SQLRevisionRepository'
+import { SQLRevisionMetadataPersistenceMapper } from '../Mapping/Persistence/SQL/SQLRevisionMetadataPersistenceMapper'
+import { SQLRevisionPersistenceMapper } from '../Mapping/Persistence/SQL/SQLRevisionPersistenceMapper'
 
 export class ContainerConfigLoader {
   async load(configuration?: {
@@ -77,6 +81,8 @@ export class ContainerConfigLoader {
     env.load()
 
     const isConfiguredForHomeServer = env.get('MODE', true) === 'home-server'
+    const isConfiguredForSelfHosting = env.get('MODE', true) === 'self-hosted'
+    const isConfiguredForHomeServerOrSelfHosting = isConfiguredForHomeServer || isConfiguredForSelfHosting
     const isSecondaryDatabaseEnabled = env.get('SECONDARY_DB_ENABLED', true) === 'true'
 
     const container = new Container({
@@ -197,9 +203,17 @@ export class ContainerConfigLoader {
       .toConstantValue(new DomainEventFactory(container.get(TYPES.Revisions_Timer)))
 
     // Map
+    container
+      .bind<MapperInterface<RevisionMetadata, SQLLegacyRevision>>(
+        TYPES.Revisions_SQLLegacyRevisionMetadataPersistenceMapper,
+      )
+      .toConstantValue(new SQLLegacyRevisionMetadataPersistenceMapper())
     container
       .bind<MapperInterface<RevisionMetadata, SQLRevision>>(TYPES.Revisions_SQLRevisionMetadataPersistenceMapper)
       .toConstantValue(new SQLRevisionMetadataPersistenceMapper())
+    container
+      .bind<MapperInterface<Revision, SQLLegacyRevision>>(TYPES.Revisions_SQLLegacyRevisionPersistenceMapper)
+      .toConstantValue(new SQLLegacyRevisionPersistenceMapper())
     container
       .bind<MapperInterface<Revision, SQLRevision>>(TYPES.Revisions_SQLRevisionPersistenceMapper)
       .toConstantValue(new SQLRevisionPersistenceMapper())
@@ -213,22 +227,36 @@ export class ContainerConfigLoader {
       .toConstantValue(new MongoDBRevisionPersistenceMapper())
 
     // ORM
+    container
+      .bind<Repository<SQLLegacyRevision>>(TYPES.Revisions_ORMLegacyRevisionRepository)
+      .toDynamicValue(() => appDataSource.getRepository(SQLLegacyRevision))
     container
       .bind<Repository<SQLRevision>>(TYPES.Revisions_ORMRevisionRepository)
-      .toDynamicValue(() => appDataSource.getRepository(SQLRevision))
+      .toConstantValue(appDataSource.getRepository(SQLRevision))
 
     // Repositories
     container
       .bind<RevisionRepositoryInterface>(TYPES.Revisions_SQLRevisionRepository)
       .toConstantValue(
-        new SQLRevisionRepository(
-          container.get<Repository<SQLRevision>>(TYPES.Revisions_ORMRevisionRepository),
-          container.get<MapperInterface<RevisionMetadata, SQLRevision>>(
-            TYPES.Revisions_SQLRevisionMetadataPersistenceMapper,
-          ),
-          container.get<MapperInterface<Revision, SQLRevision>>(TYPES.Revisions_SQLRevisionPersistenceMapper),
-          container.get<winston.Logger>(TYPES.Revisions_Logger),
-        ),
+        isConfiguredForHomeServerOrSelfHosting
+          ? new SQLRevisionRepository(
+              container.get<Repository<SQLRevision>>(TYPES.Revisions_ORMRevisionRepository),
+              container.get<MapperInterface<RevisionMetadata, SQLRevision>>(
+                TYPES.Revisions_SQLRevisionMetadataPersistenceMapper,
+              ),
+              container.get<MapperInterface<Revision, SQLRevision>>(TYPES.Revisions_SQLRevisionPersistenceMapper),
+              container.get<winston.Logger>(TYPES.Revisions_Logger),
+            )
+          : new SQLLegacyRevisionRepository(
+              container.get<Repository<SQLLegacyRevision>>(TYPES.Revisions_ORMLegacyRevisionRepository),
+              container.get<MapperInterface<RevisionMetadata, SQLLegacyRevision>>(
+                TYPES.Revisions_SQLLegacyRevisionMetadataPersistenceMapper,
+              ),
+              container.get<MapperInterface<Revision, SQLLegacyRevision>>(
+                TYPES.Revisions_SQLLegacyRevisionPersistenceMapper,
+              ),
+              container.get<winston.Logger>(TYPES.Revisions_Logger),
+            ),
       )
 
     if (isSecondaryDatabaseEnabled) {

+ 13 - 3
packages/revisions/src/Bootstrap/DataSource.ts

@@ -1,11 +1,12 @@
 import { DataSource, EntityTarget, LoggerOptions, MongoRepository, ObjectLiteral, Repository } from 'typeorm'
 import { MysqlConnectionOptions } from 'typeorm/driver/mysql/MysqlConnectionOptions'
 
-import { SQLRevision } from '../Infra/TypeORM/SQL/SQLRevision'
+import { SQLLegacyRevision } from '../Infra/TypeORM/SQL/SQLLegacyRevision'
 
 import { Env } from './Env'
 import { SqliteConnectionOptions } from 'typeorm/driver/sqlite/SqliteConnectionOptions'
 import { MongoDBRevision } from '../Infra/TypeORM/MongoDB/MongoDBRevision'
+import { SQLRevision } from '../Infra/TypeORM/SQL/SQLRevision'
 
 export class AppDataSource {
   private _dataSource: DataSource | undefined
@@ -65,14 +66,23 @@ export class AppDataSource {
 
     const isConfiguredForMySQL = this.env.get('DB_TYPE') === 'mysql'
 
+    const isConfiguredForHomeServerOrSelfHosting =
+      this.env.get('MODE', true) === 'home-server' || this.env.get('MODE', true) === 'self-hosted'
+
     const maxQueryExecutionTime = this.env.get('DB_MAX_QUERY_EXECUTION_TIME', true)
       ? +this.env.get('DB_MAX_QUERY_EXECUTION_TIME', true)
       : 45_000
 
+    const migrationsSourceDirectoryName = isConfiguredForMySQL
+      ? isConfiguredForHomeServerOrSelfHosting
+        ? 'mysql'
+        : 'mysql-legacy'
+      : 'sqlite'
+
     const commonDataSourceOptions = {
       maxQueryExecutionTime,
-      entities: [SQLRevision],
-      migrations: [`${__dirname}/../../migrations/${isConfiguredForMySQL ? 'mysql' : 'sqlite'}/*.js`],
+      entities: [isConfiguredForHomeServerOrSelfHosting ? SQLRevision : SQLLegacyRevision],
+      migrations: [`${__dirname}/../../migrations/${migrationsSourceDirectoryName}/*.js`],
       migrationsRun: true,
       logging: <LoggerOptions>this.env.get('DB_DEBUG_LEVEL', true) ?? 'info',
     }

+ 7 - 0
packages/revisions/src/Bootstrap/MigrationsDataSource.ts

@@ -0,0 +1,7 @@
+import { AppDataSource } from './DataSource'
+import { Env } from './Env'
+
+const env: Env = new Env()
+env.load()
+
+export const MigrationsDataSource = new AppDataSource(env).dataSource

+ 5 - 0
packages/revisions/src/Bootstrap/Types.ts

@@ -6,7 +6,11 @@ const TYPES = {
   Revisions_S3: Symbol.for('Revisions_S3'),
   Revisions_Env: Symbol.for('Revisions_Env'),
   // Map
+  Revisions_SQLLegacyRevisionMetadataPersistenceMapper: Symbol.for(
+    'Revisions_SQLLegacyRevisionMetadataPersistenceMapper',
+  ),
   Revisions_SQLRevisionMetadataPersistenceMapper: Symbol.for('Revisions_SQLRevisionMetadataPersistenceMapper'),
+  Revisions_SQLLegacyRevisionPersistenceMapper: Symbol.for('Revisions_SQLLegacyRevisionPersistenceMapper'),
   Revisions_SQLRevisionPersistenceMapper: Symbol.for('Revisions_SQLRevisionPersistenceMapper'),
   Revisions_MongoDBRevisionMetadataPersistenceMapper: Symbol.for('Revisions_MongoDBRevisionMetadataPersistenceMapper'),
   Revisions_MongoDBRevisionPersistenceMapper: Symbol.for('Revisions_MongoDBRevisionPersistenceMapper'),
@@ -15,6 +19,7 @@ const TYPES = {
   Revisions_RevisionMetadataHttpMapper: Symbol.for('Revisions_RevisionMetadataHttpMapper'),
   // ORM
   Revisions_ORMRevisionRepository: Symbol.for('Revisions_ORMRevisionRepository'),
+  Revisions_ORMLegacyRevisionRepository: Symbol.for('Revisions_ORMLegacyRevisionRepository'),
   // Mongo
   Revisions_ORMMongoRevisionRepository: Symbol.for('Revisions_ORMMongoRevisionRepository'),
   // Repositories

+ 3 - 0
packages/revisions/src/Domain/KeySystem/KeySystemAssocationProps.ts

@@ -0,0 +1,3 @@
+export interface KeySystemAssociationProps {
+  keySystemIdentifier: string
+}

+ 15 - 0
packages/revisions/src/Domain/KeySystem/KeySystemAssociation.spec.ts

@@ -0,0 +1,15 @@
+import { KeySystemAssociation } from './KeySystemAssociation'
+
+describe('KeySystemAssociation', () => {
+  it('should create a value object', () => {
+    const entityOrError = KeySystemAssociation.create('00000000-0000-0000-0000-000000000000')
+
+    expect(entityOrError.isFailed()).toBeFalsy()
+  })
+
+  it('should fail to create a value object with an empty key system identifier', () => {
+    const entityOrError = KeySystemAssociation.create('')
+
+    expect(entityOrError.isFailed()).toBeTruthy()
+  })
+})

+ 18 - 0
packages/revisions/src/Domain/KeySystem/KeySystemAssociation.ts

@@ -0,0 +1,18 @@
+import { Result, Validator, ValueObject } from '@standardnotes/domain-core'
+
+import { KeySystemAssociationProps } from './KeySystemAssocationProps'
+
+export class KeySystemAssociation extends ValueObject<KeySystemAssociationProps> {
+  private constructor(props: KeySystemAssociationProps) {
+    super(props)
+  }
+
+  static create(keySystemIdentifier: string): Result<KeySystemAssociation> {
+    const validationResult = Validator.isNotEmptyString(keySystemIdentifier)
+    if (validationResult.isFailed()) {
+      return Result.fail<KeySystemAssociation>(validationResult.getError())
+    }
+
+    return Result.ok<KeySystemAssociation>(new KeySystemAssociation({ keySystemIdentifier }))
+  }
+}

+ 5 - 0
packages/revisions/src/Domain/Revision/RevisionProps.ts

@@ -1,5 +1,8 @@
 import { ContentType, Dates, Uuid } from '@standardnotes/domain-core'
 
+import { SharedVaultAssociation } from '../SharedVault/SharedVaultAssociation'
+import { KeySystemAssociation } from '../KeySystem/KeySystemAssociation'
+
 export interface RevisionProps {
   itemUuid: Uuid
   userUuid: Uuid | null
@@ -10,4 +13,6 @@ export interface RevisionProps {
   authHash: string | null
   creationDate: Date
   dates: Dates
+  sharedVaultAssociation?: SharedVaultAssociation
+  keySystemAssociation?: KeySystemAssociation
 }

+ 1 - 1
packages/revisions/src/Domain/Revision/RevisionRepositoryInterface.ts

@@ -9,7 +9,7 @@ export interface RevisionRepositoryInterface {
   removeOneByUuid(revisionUuid: Uuid, userUuid: Uuid): Promise<void>
   findOneByUuid(revisionUuid: Uuid, userUuid: Uuid): Promise<Revision | null>
   findByItemUuid(itemUuid: Uuid): Promise<Array<Revision>>
-  findMetadataByItemId(itemUuid: Uuid, userUuid: Uuid): Promise<Array<RevisionMetadata>>
+  findMetadataByItemId(itemUuid: Uuid, userUuid: Uuid, sharedVaultUuids: Uuid[]): Promise<Array<RevisionMetadata>>
   updateUserUuid(itemUuid: Uuid, userUuid: Uuid): Promise<void>
   findByUserUuid(dto: { userUuid: Uuid; offset?: number; limit?: number }): Promise<Array<Revision>>
   insert(revision: Revision): Promise<boolean>

+ 14 - 0
packages/revisions/src/Domain/SharedVault/SharedVaultAssociation.spec.ts

@@ -0,0 +1,14 @@
+import { Uuid } from '@standardnotes/domain-core'
+
+import { SharedVaultAssociation } from './SharedVaultAssociation'
+
+describe('SharedVaultAssociation', () => {
+  it('should create a value object', () => {
+    const entityOrError = SharedVaultAssociation.create({
+      editedBy: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+      sharedVaultUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
+    })
+
+    expect(entityOrError.isFailed()).toBeFalsy()
+  })
+})

+ 13 - 0
packages/revisions/src/Domain/SharedVault/SharedVaultAssociation.ts

@@ -0,0 +1,13 @@
+import { Result, ValueObject } from '@standardnotes/domain-core'
+
+import { SharedVaultAssociationProps } from './SharedVaultAssociationProps'
+
+export class SharedVaultAssociation extends ValueObject<SharedVaultAssociationProps> {
+  private constructor(props: SharedVaultAssociationProps) {
+    super(props)
+  }
+
+  static create(props: SharedVaultAssociationProps): Result<SharedVaultAssociation> {
+    return Result.ok<SharedVaultAssociation>(new SharedVaultAssociation(props))
+  }
+}

+ 6 - 0
packages/revisions/src/Domain/SharedVault/SharedVaultAssociationProps.ts

@@ -0,0 +1,6 @@
+import { Uuid } from '@standardnotes/domain-core'
+
+export interface SharedVaultAssociationProps {
+  editedBy: Uuid
+  sharedVaultUuid: Uuid
+}

+ 15 - 0
packages/revisions/src/Domain/UseCase/GetRevisionsMetada/GetRevisionsMetada.spec.ts

@@ -22,17 +22,30 @@ describe('GetRevisionsMetada', () => {
       itemUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d',
       userUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d',
       roleNames: ['CORE_USER'],
+      sharedVaultUuids: ['84c0f8e8-544a-4c7e-9adf-26209303bc1d'],
     })
 
     expect(result.isFailed()).toBeFalsy()
     expect(result.getValue().length).toEqual(1)
   })
 
+  it('should not return revisions metadata for an invalid shared vault uuid', async () => {
+    const result = await createUseCase().execute({
+      itemUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d',
+      userUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d',
+      roleNames: ['CORE_USER'],
+      sharedVaultUuids: ['1-2-3'],
+    })
+
+    expect(result.isFailed()).toBeTruthy()
+  })
+
   it('should not return revisions metadata for a an invalid item uuid', async () => {
     const result = await createUseCase().execute({
       itemUuid: '1-2-3',
       userUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d',
       roleNames: ['CORE_USER'],
+      sharedVaultUuids: [],
     })
 
     expect(result.isFailed()).toBeTruthy()
@@ -43,6 +56,7 @@ describe('GetRevisionsMetada', () => {
       userUuid: '1-2-3',
       itemUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d',
       roleNames: ['CORE_USER'],
+      sharedVaultUuids: [],
     })
 
     expect(result.isFailed()).toBeTruthy()
@@ -53,6 +67,7 @@ describe('GetRevisionsMetada', () => {
       itemUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d',
       userUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d',
       roleNames: ['INVALID_ROLE_NAME'],
+      sharedVaultUuids: [],
     })
 
     expect(result.isFailed()).toBeTruthy()

+ 10 - 0
packages/revisions/src/Domain/UseCase/GetRevisionsMetada/GetRevisionsMetada.ts

@@ -19,6 +19,15 @@ export class GetRevisionsMetada implements UseCaseInterface<RevisionMetadata[]>
       return Result.fail<RevisionMetadata[]>(`Could not get revisions: ${userUuidOrError.getError()}`)
     }
 
+    const sharedVaultUuids = []
+    for (const sharedVaultUuid of dto.sharedVaultUuids) {
+      const sharedVaultUuidOrError = Uuid.create(sharedVaultUuid)
+      if (sharedVaultUuidOrError.isFailed()) {
+        return Result.fail<RevisionMetadata[]>(`Could not get revisions: ${sharedVaultUuidOrError.getError()}`)
+      }
+      sharedVaultUuids.push(sharedVaultUuidOrError.getValue())
+    }
+
     const roleNamesOrError = RoleNameCollection.create(dto.roleNames)
     if (roleNamesOrError.isFailed()) {
       return Result.fail(roleNamesOrError.getError())
@@ -30,6 +39,7 @@ export class GetRevisionsMetada implements UseCaseInterface<RevisionMetadata[]>
     const revisionsMetdata = await revisionRepository.findMetadataByItemId(
       itemUuidOrError.getValue(),
       userUuidOrError.getValue(),
+      sharedVaultUuids,
     )
 
     return Result.ok<RevisionMetadata[]>(revisionsMetdata)

+ 1 - 0
packages/revisions/src/Domain/UseCase/GetRevisionsMetada/GetRevisionsMetadaDTO.ts

@@ -1,5 +1,6 @@
 export interface GetRevisionsMetadaDTO {
   itemUuid: string
+  sharedVaultUuids: string[]
   userUuid: string
   roleNames: string[]
 }

+ 3 - 0
packages/revisions/src/Infra/InversifyExpress/Base/BaseRevisionsController.ts

@@ -38,6 +38,9 @@ export class BaseRevisionsController extends BaseHttpController {
       itemUuid: request.params.itemUuid,
       userUuid: response.locals.user.uuid,
       roleNames: response.locals.roles.map((role: Role) => role.name),
+      sharedVaultUuids: response.locals.belongsToSharedVaults.map(
+        (association: { shared_vault_uuid: string; permission: string }) => association.shared_vault_uuid,
+      ),
     })
 
     if (revisionMetadataOrError.isFailed()) {

+ 1 - 0
packages/revisions/src/Infra/InversifyExpress/Middleware/ApiGatewayAuthMiddleware.ts

@@ -47,6 +47,7 @@ export class ApiGatewayAuthMiddleware extends BaseMiddleware {
       response.locals.roles = token.roles
       response.locals.session = token.session
       response.locals.readOnlyAccess = token.session?.readonly_access ?? false
+      response.locals.belongsToSharedVaults = token.belongs_to_shared_vaults ?? []
 
       return next()
     } catch (error) {

+ 10 - 0
packages/revisions/src/Infra/TypeORM/MongoDB/MongoDBRevision.ts

@@ -37,4 +37,14 @@ export class MongoDBRevision {
 
   @Column()
   declare updatedAt: Date
+
+  @Column()
+  declare editedBy: string | null
+
+  @Column()
+  @Index('index_revisions_on_shared_vault_uuid')
+  declare sharedVaultUuid: string | null
+
+  @Column()
+  declare keySystemIdentifier: string | null
 }

+ 30 - 10
packages/revisions/src/Infra/TypeORM/MongoDB/MongoDBRevisionRepository.ts

@@ -86,16 +86,36 @@ export class MongoDBRevisionRepository implements RevisionRepositoryInterface {
     return revisions
   }
 
-  async findMetadataByItemId(itemUuid: Uuid, userUuid: Uuid): Promise<RevisionMetadata[]> {
-    const persistence = await this.mongoRepository.find({
-      select: ['_id', 'contentType', 'createdAt', 'updatedAt'],
-      where: {
-        $and: [{ itemUuid: { $eq: itemUuid.value } }, { userUuid: { $eq: userUuid.value } }],
-      },
-      order: {
-        createdAt: 'DESC',
-      },
-    })
+  async findMetadataByItemId(
+    itemUuid: Uuid,
+    userUuid: Uuid,
+    sharedVaultUuids: Uuid[],
+  ): Promise<Array<RevisionMetadata>> {
+    let persistence = []
+    if (sharedVaultUuids.length > 0) {
+      persistence = await this.mongoRepository.find({
+        select: ['_id', 'contentType', 'createdAt', 'updatedAt'],
+        where: {
+          $and: [
+            { itemUuid: { $eq: itemUuid.value } },
+            { sharedVaultUuid: { $in: sharedVaultUuids.map((uuid) => uuid.value) } },
+          ],
+        },
+        order: {
+          createdAt: 'DESC',
+        },
+      })
+    } else {
+      persistence = await this.mongoRepository.find({
+        select: ['_id', 'contentType', 'createdAt', 'updatedAt'],
+        where: {
+          $and: [{ itemUuid: { $eq: itemUuid.value } }, { userUuid: { $eq: userUuid.value } }],
+        },
+        order: {
+          createdAt: 'DESC',
+        },
+      })
+    }
 
     const revisions: RevisionMetadata[] = []
 

+ 83 - 0
packages/revisions/src/Infra/TypeORM/SQL/SQLLegacyRevision.ts

@@ -0,0 +1,83 @@
+import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'
+
+@Entity({ name: 'revisions' })
+export class SQLLegacyRevision {
+  @PrimaryGeneratedColumn('uuid')
+  declare uuid: string
+
+  @Column({
+    name: 'item_uuid',
+    length: 36,
+  })
+  @Index('item_uuid')
+  declare itemUuid: string
+
+  @Column({
+    name: 'user_uuid',
+    length: 36,
+    type: 'varchar',
+    nullable: true,
+  })
+  @Index('user_uuid')
+  declare userUuid: string | null
+
+  @Column({
+    type: 'text',
+    nullable: true,
+  })
+  declare content: string | null
+
+  @Column({
+    name: 'content_type',
+    type: 'varchar',
+    length: 255,
+    nullable: true,
+  })
+  declare contentType: string | null
+
+  @Column({
+    type: 'varchar',
+    name: 'items_key_id',
+    length: 255,
+    nullable: true,
+  })
+  declare itemsKeyId: string | null
+
+  @Column({
+    name: 'enc_item_key',
+    type: 'text',
+    nullable: true,
+  })
+  declare encItemKey: string | null
+
+  @Column({
+    name: 'auth_hash',
+    type: 'varchar',
+    length: 255,
+    nullable: true,
+  })
+  declare authHash: string | null
+
+  @Column({
+    name: 'creation_date',
+    type: 'date',
+    nullable: true,
+  })
+  declare creationDate: Date
+
+  @Column({
+    name: 'created_at',
+    type: 'datetime',
+    precision: 6,
+    nullable: true,
+  })
+  declare createdAt: Date
+
+  @Column({
+    name: 'updated_at',
+    type: 'datetime',
+    precision: 6,
+    nullable: true,
+  })
+  declare updatedAt: Date
+}

+ 146 - 0
packages/revisions/src/Infra/TypeORM/SQL/SQLLegacyRevisionRepository.ts

@@ -0,0 +1,146 @@
+import { MapperInterface, Uuid } from '@standardnotes/domain-core'
+import { Repository } from 'typeorm'
+import { Logger } from 'winston'
+
+import { Revision } from '../../../Domain/Revision/Revision'
+import { RevisionMetadata } from '../../../Domain/Revision/RevisionMetadata'
+import { RevisionRepositoryInterface } from '../../../Domain/Revision/RevisionRepositoryInterface'
+import { SQLLegacyRevision } from './SQLLegacyRevision'
+
+export class SQLLegacyRevisionRepository implements RevisionRepositoryInterface {
+  constructor(
+    protected ormRepository: Repository<SQLLegacyRevision>,
+    protected revisionMetadataMapper: MapperInterface<RevisionMetadata, SQLLegacyRevision>,
+    protected revisionMapper: MapperInterface<Revision, SQLLegacyRevision>,
+    protected logger: Logger,
+  ) {}
+
+  async countByUserUuid(userUuid: Uuid): Promise<number> {
+    return this.ormRepository
+      .createQueryBuilder()
+      .where('user_uuid = :userUuid', { userUuid: userUuid.value })
+      .getCount()
+  }
+
+  async findByUserUuid(dto: { userUuid: Uuid; offset?: number; limit?: number }): Promise<Revision[]> {
+    const queryBuilder = this.ormRepository
+      .createQueryBuilder('revision')
+      .where('revision.user_uuid = :userUuid', { userUuid: dto.userUuid.value })
+      .orderBy('revision.uuid', 'ASC')
+
+    if (dto.offset !== undefined) {
+      queryBuilder.skip(dto.offset)
+    }
+
+    if (dto.limit !== undefined) {
+      queryBuilder.take(dto.limit)
+    }
+
+    const sqlRevisions = await queryBuilder.getMany()
+
+    const revisions = []
+    for (const sqlRevision of sqlRevisions) {
+      revisions.push(this.revisionMapper.toDomain(sqlRevision))
+    }
+
+    return revisions
+  }
+
+  async updateUserUuid(itemUuid: Uuid, userUuid: Uuid): Promise<void> {
+    await this.ormRepository
+      .createQueryBuilder()
+      .update()
+      .set({
+        userUuid: userUuid.value,
+      })
+      .where('item_uuid = :itemUuid', { itemUuid: itemUuid.value })
+      .execute()
+  }
+
+  async findByItemUuid(itemUuid: Uuid): Promise<Revision[]> {
+    const SQLLegacyRevisions = await this.ormRepository
+      .createQueryBuilder()
+      .where('item_uuid = :itemUuid', { itemUuid: itemUuid.value })
+      .getMany()
+
+    const revisions = []
+    for (const revision of SQLLegacyRevisions) {
+      revisions.push(this.revisionMapper.toDomain(revision))
+    }
+
+    return revisions
+  }
+
+  async removeByUserUuid(userUuid: Uuid): Promise<void> {
+    await this.ormRepository
+      .createQueryBuilder()
+      .delete()
+      .from('revisions')
+      .where('user_uuid = :userUuid', { userUuid: userUuid.value })
+      .execute()
+  }
+
+  async removeOneByUuid(revisionUuid: Uuid, userUuid: Uuid): Promise<void> {
+    await this.ormRepository
+      .createQueryBuilder()
+      .delete()
+      .from('revisions')
+      .where('uuid = :revisionUuid AND user_uuid = :userUuid', {
+        userUuid: userUuid.value,
+        revisionUuid: revisionUuid.value,
+      })
+      .execute()
+  }
+
+  async findOneByUuid(revisionUuid: Uuid, userUuid: Uuid): Promise<Revision | null> {
+    const SQLLegacyRevision = await this.ormRepository
+      .createQueryBuilder()
+      .where('uuid = :revisionUuid', { revisionUuid: revisionUuid.value })
+      .andWhere('user_uuid = :userUuid', { userUuid: userUuid.value })
+      .getOne()
+
+    if (SQLLegacyRevision === null) {
+      return null
+    }
+
+    return this.revisionMapper.toDomain(SQLLegacyRevision)
+  }
+
+  async insert(revision: Revision): Promise<boolean> {
+    const SQLLegacyRevision = this.revisionMapper.toProjection(revision)
+
+    await this.ormRepository.insert(SQLLegacyRevision)
+
+    return true
+  }
+
+  async findMetadataByItemId(
+    itemUuid: Uuid,
+    userUuid: Uuid,
+    _sharedVaultUuids: Uuid[],
+  ): Promise<Array<RevisionMetadata>> {
+    const queryBuilder = this.ormRepository
+      .createQueryBuilder()
+      .select('uuid', 'uuid')
+      .addSelect('content_type', 'contentType')
+      .addSelect('created_at', 'createdAt')
+      .addSelect('updated_at', 'updatedAt')
+      .where('item_uuid = :itemUuid', { itemUuid: itemUuid.value })
+      .andWhere('user_uuid = :userUuid', { userUuid: userUuid.value })
+      .orderBy('created_at', 'DESC')
+
+    const simplifiedRevisions = await queryBuilder.getRawMany()
+
+    this.logger.debug(
+      `Found ${simplifiedRevisions.length} revisions entries for item ${itemUuid.value}`,
+      simplifiedRevisions,
+    )
+
+    const metadata = []
+    for (const simplifiedRevision of simplifiedRevisions) {
+      metadata.push(this.revisionMetadataMapper.toDomain(simplifiedRevision))
+    }
+
+    return metadata
+  }
+}

+ 14 - 66
packages/revisions/src/Infra/TypeORM/SQL/SQLRevision.ts

@@ -1,83 +1,31 @@
-import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'
+import { Column, Entity, Index } from 'typeorm'
 
-@Entity({ name: 'revisions' })
-export class SQLRevision {
-  @PrimaryGeneratedColumn('uuid')
-  declare uuid: string
-
-  @Column({
-    name: 'item_uuid',
-    length: 36,
-  })
-  @Index('item_uuid')
-  declare itemUuid: string
-
-  @Column({
-    name: 'user_uuid',
-    length: 36,
-    type: 'varchar',
-    nullable: true,
-  })
-  @Index('user_uuid')
-  declare userUuid: string | null
-
-  @Column({
-    type: 'text',
-    nullable: true,
-  })
-  declare content: string | null
+import { SQLLegacyRevision } from './SQLLegacyRevision'
 
+@Entity({ name: 'revisions' })
+export class SQLRevision extends SQLLegacyRevision {
   @Column({
-    name: 'content_type',
     type: 'varchar',
-    length: 255,
+    name: 'edited_by',
+    length: 36,
     nullable: true,
   })
-  declare contentType: string | null
+  declare editedBy: string | null
 
   @Column({
     type: 'varchar',
-    name: 'items_key_id',
-    length: 255,
-    nullable: true,
-  })
-  declare itemsKeyId: string | null
-
-  @Column({
-    name: 'enc_item_key',
-    type: 'text',
+    name: 'shared_vault_uuid',
+    length: 36,
     nullable: true,
   })
-  declare encItemKey: string | null
+  @Index('index_revisions_on_shared_vault_uuid')
+  declare sharedVaultUuid: string | null
 
   @Column({
-    name: 'auth_hash',
     type: 'varchar',
-    length: 255,
-    nullable: true,
-  })
-  declare authHash: string | null
-
-  @Column({
-    name: 'creation_date',
-    type: 'date',
-    nullable: true,
-  })
-  declare creationDate: Date
-
-  @Column({
-    name: 'created_at',
-    type: 'datetime',
-    precision: 6,
-    nullable: true,
-  })
-  declare createdAt: Date
-
-  @Column({
-    name: 'updated_at',
-    type: 'datetime',
-    precision: 6,
+    name: 'key_system_identifier',
+    length: 36,
     nullable: true,
   })
-  declare updatedAt: Date
+  declare keySystemIdentifier: string | null
 }

+ 23 - 110
packages/revisions/src/Infra/TypeORM/SQL/SQLRevisionRepository.ts

@@ -4,127 +4,40 @@ import { Logger } from 'winston'
 
 import { Revision } from '../../../Domain/Revision/Revision'
 import { RevisionMetadata } from '../../../Domain/Revision/RevisionMetadata'
-import { RevisionRepositoryInterface } from '../../../Domain/Revision/RevisionRepositoryInterface'
+import { SQLLegacyRevisionRepository } from './SQLLegacyRevisionRepository'
 import { SQLRevision } from './SQLRevision'
 
-export class SQLRevisionRepository implements RevisionRepositoryInterface {
+export class SQLRevisionRepository extends SQLLegacyRevisionRepository {
   constructor(
-    private ormRepository: Repository<SQLRevision>,
-    private revisionMetadataMapper: MapperInterface<RevisionMetadata, SQLRevision>,
-    private revisionMapper: MapperInterface<Revision, SQLRevision>,
-    private logger: Logger,
-  ) {}
-
-  async countByUserUuid(userUuid: Uuid): Promise<number> {
-    return this.ormRepository
-      .createQueryBuilder()
-      .where('user_uuid = :userUuid', { userUuid: userUuid.value })
-      .getCount()
-  }
-
-  async findByUserUuid(dto: { userUuid: Uuid; offset?: number; limit?: number }): Promise<Revision[]> {
-    const queryBuilder = this.ormRepository
-      .createQueryBuilder('revision')
-      .where('revision.user_uuid = :userUuid', { userUuid: dto.userUuid.value })
-      .orderBy('revision.uuid', 'ASC')
-
-    if (dto.offset !== undefined) {
-      queryBuilder.skip(dto.offset)
-    }
-
-    if (dto.limit !== undefined) {
-      queryBuilder.take(dto.limit)
-    }
-
-    const sqlRevisions = await queryBuilder.getMany()
-
-    const revisions = []
-    for (const sqlRevision of sqlRevisions) {
-      revisions.push(this.revisionMapper.toDomain(sqlRevision))
-    }
-
-    return revisions
-  }
-
-  async updateUserUuid(itemUuid: Uuid, userUuid: Uuid): Promise<void> {
-    await this.ormRepository
-      .createQueryBuilder()
-      .update()
-      .set({
-        userUuid: userUuid.value,
-      })
-      .where('item_uuid = :itemUuid', { itemUuid: itemUuid.value })
-      .execute()
-  }
-
-  async findByItemUuid(itemUuid: Uuid): Promise<Revision[]> {
-    const SQLRevisions = await this.ormRepository
-      .createQueryBuilder()
-      .where('item_uuid = :itemUuid', { itemUuid: itemUuid.value })
-      .getMany()
-
-    const revisions = []
-    for (const revision of SQLRevisions) {
-      revisions.push(this.revisionMapper.toDomain(revision))
-    }
-
-    return revisions
-  }
-
-  async removeByUserUuid(userUuid: Uuid): Promise<void> {
-    await this.ormRepository
-      .createQueryBuilder()
-      .delete()
-      .from('revisions')
-      .where('user_uuid = :userUuid', { userUuid: userUuid.value })
-      .execute()
-  }
-
-  async removeOneByUuid(revisionUuid: Uuid, userUuid: Uuid): Promise<void> {
-    await this.ormRepository
-      .createQueryBuilder()
-      .delete()
-      .from('revisions')
-      .where('uuid = :revisionUuid AND user_uuid = :userUuid', {
-        userUuid: userUuid.value,
-        revisionUuid: revisionUuid.value,
-      })
-      .execute()
-  }
-
-  async findOneByUuid(revisionUuid: Uuid, userUuid: Uuid): Promise<Revision | null> {
-    const SQLRevision = await this.ormRepository
-      .createQueryBuilder()
-      .where('uuid = :revisionUuid', { revisionUuid: revisionUuid.value })
-      .andWhere('user_uuid = :userUuid', { userUuid: userUuid.value })
-      .getOne()
-
-    if (SQLRevision === null) {
-      return null
-    }
-
-    return this.revisionMapper.toDomain(SQLRevision)
-  }
-
-  async insert(revision: Revision): Promise<boolean> {
-    const SQLRevision = this.revisionMapper.toProjection(revision)
-
-    await this.ormRepository.insert(SQLRevision)
-
-    return true
-  }
-
-  async findMetadataByItemId(itemUuid: Uuid, userUuid: Uuid): Promise<Array<RevisionMetadata>> {
+    protected override ormRepository: Repository<SQLRevision>,
+    protected override revisionMetadataMapper: MapperInterface<RevisionMetadata, SQLRevision>,
+    protected override revisionMapper: MapperInterface<Revision, SQLRevision>,
+    protected override logger: Logger,
+  ) {
+    super(ormRepository, revisionMetadataMapper, revisionMapper, logger)
+  }
+
+  override async findMetadataByItemId(
+    itemUuid: Uuid,
+    userUuid: Uuid,
+    sharedVaultUuids: Uuid[],
+  ): Promise<Array<RevisionMetadata>> {
     const queryBuilder = this.ormRepository
       .createQueryBuilder()
       .select('uuid', 'uuid')
       .addSelect('content_type', 'contentType')
       .addSelect('created_at', 'createdAt')
       .addSelect('updated_at', 'updatedAt')
-      .where('item_uuid = :itemUuid', { itemUuid: itemUuid.value })
-      .andWhere('user_uuid = :userUuid', { userUuid: userUuid.value })
+      .where('item_uuid = :itemUuid AND user_uuid = :userUuid', { itemUuid: itemUuid.value, userUuid: userUuid.value })
       .orderBy('created_at', 'DESC')
 
+    if (sharedVaultUuids.length > 0) {
+      queryBuilder.orWhere('item_uuid = :itemUuid AND shared_vault_uuid IN (:...sharedVaultUuids)', {
+        itemUuid: itemUuid.value,
+        sharedVaultUuids: sharedVaultUuids.map((uuid) => uuid.value),
+      })
+    }
+
     const simplifiedRevisions = await queryBuilder.getRawMany()
 
     this.logger.debug(

+ 37 - 0
packages/revisions/src/Mapping/Backup/RevisionItemStringMapper.ts

@@ -1,6 +1,8 @@
 import { MapperInterface, Dates, Uuid, ContentType } from '@standardnotes/domain-core'
 
 import { Revision } from '../../Domain/Revision/Revision'
+import { SharedVaultAssociation } from '../../Domain/SharedVault/SharedVaultAssociation'
+import { KeySystemAssociation } from '../../Domain/KeySystem/KeySystemAssociation'
 
 export class RevisionItemStringMapper implements MapperInterface<Revision, string> {
   toDomain(projection: string): Revision {
@@ -24,6 +26,39 @@ export class RevisionItemStringMapper implements MapperInterface<Revision, strin
     }
     const userUuid = userUuidOrError.getValue()
 
+    let sharedVaultAssociation: SharedVaultAssociation | undefined = undefined
+    if (item.shared_vault_uuid && item.last_edited_by) {
+      const sharedVaultUuidOrError = Uuid.create(item.shared_vault_uuid)
+      if (sharedVaultUuidOrError.isFailed()) {
+        throw new Error(`Failed to create revision from projection: ${sharedVaultUuidOrError.getError()}`)
+      }
+      const sharedVaultUuid = sharedVaultUuidOrError.getValue()
+
+      const lastEditedByOrError = Uuid.create(item.last_edited_by)
+      if (lastEditedByOrError.isFailed()) {
+        throw new Error(`Failed to create revision from projection: ${lastEditedByOrError.getError()}`)
+      }
+      const lastEditedBy = lastEditedByOrError.getValue()
+
+      const sharedVaultAssociationOrError = SharedVaultAssociation.create({
+        sharedVaultUuid,
+        editedBy: lastEditedBy,
+      })
+      if (sharedVaultAssociationOrError.isFailed()) {
+        throw new Error(`Failed to create revision from projection: ${sharedVaultAssociationOrError.getError()}`)
+      }
+      sharedVaultAssociation = sharedVaultAssociationOrError.getValue()
+    }
+
+    let keySystemAssociation: KeySystemAssociation | undefined = undefined
+    if (item.key_system_identifier) {
+      const keySystemAssociationOrError = KeySystemAssociation.create(item.key_system_identifier)
+      if (keySystemAssociationOrError.isFailed()) {
+        throw new Error(`Failed to create revision from projection: ${keySystemAssociationOrError.getError()}`)
+      }
+      keySystemAssociation = keySystemAssociationOrError.getValue()
+    }
+
     const revisionOrError = Revision.create({
       itemUuid,
       userUuid,
@@ -34,6 +69,8 @@ export class RevisionItemStringMapper implements MapperInterface<Revision, strin
       encItemKey: item.enc_item_key,
       creationDate: new Date(),
       dates: Dates.create(new Date(), new Date()).getValue(),
+      sharedVaultAssociation,
+      keySystemAssociation,
     })
 
     if (revisionOrError.isFailed()) {

+ 43 - 0
packages/revisions/src/Mapping/Persistence/SQL/SQLLegacyRevisionMetadataPersistenceMapper.ts

@@ -0,0 +1,43 @@
+import { MapperInterface, Dates, UniqueEntityId, ContentType } from '@standardnotes/domain-core'
+
+import { RevisionMetadata } from '../../../Domain/Revision/RevisionMetadata'
+import { SQLLegacyRevision } from '../../../Infra/TypeORM/SQL/SQLLegacyRevision'
+
+export class SQLLegacyRevisionMetadataPersistenceMapper
+  implements MapperInterface<RevisionMetadata, SQLLegacyRevision>
+{
+  toDomain(projection: SQLLegacyRevision): RevisionMetadata {
+    const contentTypeOrError = ContentType.create(projection.contentType)
+    if (contentTypeOrError.isFailed()) {
+      throw new Error(`Could not create content type: ${contentTypeOrError.getError()}`)
+    }
+    const contentType = contentTypeOrError.getValue()
+
+    const createdAt = projection.createdAt instanceof Date ? projection.createdAt : new Date(projection.createdAt)
+    const updatedAt = projection.updatedAt instanceof Date ? projection.updatedAt : new Date(projection.updatedAt)
+
+    const datesOrError = Dates.create(createdAt, updatedAt)
+    if (datesOrError.isFailed()) {
+      throw new Error(`Could not create dates: ${datesOrError.getError()}`)
+    }
+    const dates = datesOrError.getValue()
+
+    const revisionMetadataOrError = RevisionMetadata.create(
+      {
+        contentType,
+        dates,
+      },
+      new UniqueEntityId(projection.uuid),
+    )
+
+    if (revisionMetadataOrError.isFailed()) {
+      throw new Error(`Could not create revision metdata: ${revisionMetadataOrError.getError()}`)
+    }
+
+    return revisionMetadataOrError.getValue()
+  }
+
+  toProjection(_domain: RevisionMetadata): SQLLegacyRevision {
+    throw new Error('Method not implemented.')
+  }
+}

+ 73 - 0
packages/revisions/src/Mapping/Persistence/SQL/SQLLegacyRevisionPersistenceMapper.ts

@@ -0,0 +1,73 @@
+import { MapperInterface, Dates, UniqueEntityId, Uuid, ContentType } from '@standardnotes/domain-core'
+
+import { Revision } from '../../../Domain/Revision/Revision'
+import { SQLLegacyRevision } from '../../../Infra/TypeORM/SQL/SQLLegacyRevision'
+
+export class SQLLegacyRevisionPersistenceMapper implements MapperInterface<Revision, SQLLegacyRevision> {
+  toDomain(projection: SQLLegacyRevision): Revision {
+    const contentTypeOrError = ContentType.create(projection.contentType)
+    if (contentTypeOrError.isFailed()) {
+      throw new Error(`Could not map typeorm revision to domain revision: ${contentTypeOrError.getError()}`)
+    }
+    const contentType = contentTypeOrError.getValue()
+
+    const datesOrError = Dates.create(projection.createdAt, projection.updatedAt)
+    if (datesOrError.isFailed()) {
+      throw new Error(`Could not map typeorm revision to domain revision: ${datesOrError.getError()}`)
+    }
+    const dates = datesOrError.getValue()
+
+    const itemUuidOrError = Uuid.create(projection.itemUuid)
+    if (itemUuidOrError.isFailed()) {
+      throw new Error(`Could not map typeorm revision to domain revision: ${itemUuidOrError.getError()}`)
+    }
+    const itemUuid = itemUuidOrError.getValue()
+
+    let userUuid = null
+    if (projection.userUuid !== null) {
+      const userUuidOrError = Uuid.create(projection.userUuid)
+      if (userUuidOrError.isFailed()) {
+        throw new Error(`Could not map typeorm revision to domain revision: ${userUuidOrError.getError()}`)
+      }
+      userUuid = userUuidOrError.getValue()
+    }
+
+    const revisionOrError = Revision.create(
+      {
+        authHash: projection.authHash,
+        content: projection.content,
+        contentType,
+        creationDate: projection.creationDate,
+        encItemKey: projection.encItemKey,
+        itemsKeyId: projection.itemsKeyId,
+        itemUuid,
+        userUuid,
+        dates,
+      },
+      new UniqueEntityId(projection.uuid),
+    )
+    if (revisionOrError.isFailed()) {
+      throw new Error(`Could not map typeorm revision to domain revision: ${revisionOrError.getError()}`)
+    }
+
+    return revisionOrError.getValue()
+  }
+
+  toProjection(domain: Revision): SQLLegacyRevision {
+    const sqlRevision = new SQLLegacyRevision()
+
+    sqlRevision.authHash = domain.props.authHash
+    sqlRevision.content = domain.props.content
+    sqlRevision.contentType = domain.props.contentType.value
+    sqlRevision.createdAt = domain.props.dates.createdAt
+    sqlRevision.updatedAt = domain.props.dates.updatedAt
+    sqlRevision.creationDate = domain.props.creationDate
+    sqlRevision.encItemKey = domain.props.encItemKey
+    sqlRevision.itemUuid = domain.props.itemUuid.value
+    sqlRevision.itemsKeyId = domain.props.itemsKeyId
+    sqlRevision.userUuid = domain.props.userUuid ? domain.props.userUuid.value : null
+    sqlRevision.uuid = domain.id.toString()
+
+    return sqlRevision
+  }
+}

+ 46 - 0
packages/revisions/src/Mapping/Persistence/SQL/SQLRevisionPersistenceMapper.ts

@@ -2,6 +2,8 @@ import { MapperInterface, Dates, UniqueEntityId, Uuid, ContentType } from '@stan
 
 import { Revision } from '../../../Domain/Revision/Revision'
 import { SQLRevision } from '../../../Infra/TypeORM/SQL/SQLRevision'
+import { SharedVaultAssociation } from '../../../Domain/SharedVault/SharedVaultAssociation'
+import { KeySystemAssociation } from '../../../Domain/KeySystem/KeySystemAssociation'
 
 export class SQLRevisionPersistenceMapper implements MapperInterface<Revision, SQLRevision> {
   toDomain(projection: SQLRevision): Revision {
@@ -32,6 +34,39 @@ export class SQLRevisionPersistenceMapper implements MapperInterface<Revision, S
       userUuid = userUuidOrError.getValue()
     }
 
+    let sharedVaultAssociation: SharedVaultAssociation | undefined = undefined
+    if (projection.sharedVaultUuid && projection.editedBy) {
+      const sharedVaultUuidOrError = Uuid.create(projection.sharedVaultUuid)
+      if (sharedVaultUuidOrError.isFailed()) {
+        throw new Error(`Failed to create revision from projection: ${sharedVaultUuidOrError.getError()}`)
+      }
+      const sharedVaultUuid = sharedVaultUuidOrError.getValue()
+
+      const lastEditedByOrError = Uuid.create(projection.editedBy)
+      if (lastEditedByOrError.isFailed()) {
+        throw new Error(`Failed to create revision from projection: ${lastEditedByOrError.getError()}`)
+      }
+      const lastEditedBy = lastEditedByOrError.getValue()
+
+      const sharedVaultAssociationOrError = SharedVaultAssociation.create({
+        sharedVaultUuid,
+        editedBy: lastEditedBy,
+      })
+      if (sharedVaultAssociationOrError.isFailed()) {
+        throw new Error(`Failed to create revision from projection: ${sharedVaultAssociationOrError.getError()}`)
+      }
+      sharedVaultAssociation = sharedVaultAssociationOrError.getValue()
+    }
+
+    let keySystemAssociation: KeySystemAssociation | undefined = undefined
+    if (projection.keySystemIdentifier) {
+      const keySystemAssociationOrError = KeySystemAssociation.create(projection.keySystemIdentifier)
+      if (keySystemAssociationOrError.isFailed()) {
+        throw new Error(`Failed to create revision from projection: ${keySystemAssociationOrError.getError()}`)
+      }
+      keySystemAssociation = keySystemAssociationOrError.getValue()
+    }
+
     const revisionOrError = Revision.create(
       {
         authHash: projection.authHash,
@@ -43,6 +78,8 @@ export class SQLRevisionPersistenceMapper implements MapperInterface<Revision, S
         itemUuid,
         userUuid,
         dates,
+        sharedVaultAssociation,
+        keySystemAssociation,
       },
       new UniqueEntityId(projection.uuid),
     )
@@ -67,6 +104,15 @@ export class SQLRevisionPersistenceMapper implements MapperInterface<Revision, S
     sqlRevision.itemsKeyId = domain.props.itemsKeyId
     sqlRevision.userUuid = domain.props.userUuid ? domain.props.userUuid.value : null
     sqlRevision.uuid = domain.id.toString()
+    sqlRevision.sharedVaultUuid = domain.props.sharedVaultAssociation
+      ? domain.props.sharedVaultAssociation.props.sharedVaultUuid.value
+      : null
+    sqlRevision.editedBy = domain.props.sharedVaultAssociation
+      ? domain.props.sharedVaultAssociation.props.editedBy.value
+      : null
+    sqlRevision.keySystemIdentifier = domain.props.keySystemAssociation
+      ? domain.props.keySystemAssociation.props.keySystemIdentifier
+      : null
 
     return sqlRevision
   }

+ 4 - 0
packages/security/src/Domain/Token/CrossServiceTokenData.ts

@@ -5,6 +5,10 @@ export type CrossServiceTokenData = {
     uuid: string
     email: string
   }
+  belongs_to_shared_vaults?: Array<{
+    shared_vault_uuid: string
+    permission: string
+  }>
   shared_vault_owner_context?: {
     upload_bytes_limit: number
   }

+ 16 - 8
packages/syncing-server/src/Bootstrap/Container.ts

@@ -58,7 +58,12 @@ import { ItemBackupServiceInterface } from '../Domain/Item/ItemBackupServiceInte
 import { FSItemBackupService } from '../Infra/FS/FSItemBackupService'
 import { AuthHttpService } from '../Infra/HTTP/AuthHttpService'
 import { S3ItemBackupService } from '../Infra/S3/S3ItemBackupService'
-import { ControllerContainer, ControllerContainerInterface, MapperInterface } from '@standardnotes/domain-core'
+import {
+  ControllerContainer,
+  ControllerContainerInterface,
+  MapperInterface,
+  SharedVaultUser,
+} from '@standardnotes/domain-core'
 import { BaseItemsController } from '../Infra/InversifyExpressUtils/Base/BaseItemsController'
 import { Transform } from 'stream'
 import { SQLLegacyItem } from '../Infra/TypeORM/SQLLegacyItem'
@@ -87,7 +92,6 @@ import { TypeORMSharedVaultUser } from '../Infra/TypeORM/TypeORMSharedVaultUser'
 import { SharedVaultRepositoryInterface } from '../Domain/SharedVault/SharedVaultRepositoryInterface'
 import { SharedVaultPersistenceMapper } from '../Mapping/Persistence/SharedVaultPersistenceMapper'
 import { SharedVault } from '../Domain/SharedVault/SharedVault'
-import { SharedVaultUser } from '../Domain/SharedVault/User/SharedVaultUser'
 import { SharedVaultUserPersistenceMapper } from '../Mapping/Persistence/SharedVaultUserPersistenceMapper'
 import { SharedVaultInvite } from '../Domain/SharedVault/User/Invite/SharedVaultInvite'
 import { SharedVaultInvitePersistenceMapper } from '../Mapping/Persistence/SharedVaultInvitePersistenceMapper'
@@ -699,9 +703,11 @@ export class ContainerConfigLoader {
       .bind<AddUserToSharedVault>(TYPES.Sync_AddUserToSharedVault)
       .toConstantValue(
         new AddUserToSharedVault(
-          container.get(TYPES.Sync_SharedVaultRepository),
-          container.get(TYPES.Sync_SharedVaultUserRepository),
-          container.get(TYPES.Sync_Timer),
+          container.get<SharedVaultRepositoryInterface>(TYPES.Sync_SharedVaultRepository),
+          container.get<SharedVaultUserRepositoryInterface>(TYPES.Sync_SharedVaultUserRepository),
+          container.get<TimerInterface>(TYPES.Sync_Timer),
+          container.get<DomainEventFactoryInterface>(TYPES.Sync_DomainEventFactory),
+          container.get<DomainEventPublisherInterface>(TYPES.Sync_DomainEventPublisher),
         ),
       )
     container
@@ -746,9 +752,11 @@ export class ContainerConfigLoader {
       .bind<RemoveUserFromSharedVault>(TYPES.Sync_RemoveSharedVaultUser)
       .toConstantValue(
         new RemoveUserFromSharedVault(
-          container.get(TYPES.Sync_SharedVaultUserRepository),
-          container.get(TYPES.Sync_SharedVaultRepository),
-          container.get(TYPES.Sync_AddNotificationForUser),
+          container.get<SharedVaultUserRepositoryInterface>(TYPES.Sync_SharedVaultUserRepository),
+          container.get<SharedVaultRepositoryInterface>(TYPES.Sync_SharedVaultRepository),
+          container.get<AddNotificationForUser>(TYPES.Sync_AddNotificationForUser),
+          container.get<DomainEventFactoryInterface>(TYPES.Sync_DomainEventFactory),
+          container.get<DomainEventPublisherInterface>(TYPES.Sync_DomainEventPublisher),
         ),
       )
     container

+ 40 - 0
packages/syncing-server/src/Domain/Event/DomainEventFactory.ts

@@ -9,7 +9,9 @@ import {
   NotificationAddedForUserEvent,
   RevisionsCopyRequestedEvent,
   TransitionStatusUpdatedEvent,
+  UserAddedToSharedVaultEvent,
   UserInvitedToSharedVaultEvent,
+  UserRemovedFromSharedVaultEvent,
   WebSocketMessageRequestedEvent,
 } from '@standardnotes/domain-events'
 import { TimerInterface } from '@standardnotes/time'
@@ -18,6 +20,44 @@ import { DomainEventFactoryInterface } from './DomainEventFactoryInterface'
 export class DomainEventFactory implements DomainEventFactoryInterface {
   constructor(private timer: TimerInterface) {}
 
+  createUserRemovedFromSharedVaultEvent(dto: {
+    sharedVaultUuid: string
+    userUuid: string
+  }): UserRemovedFromSharedVaultEvent {
+    return {
+      type: 'USER_REMOVED_FROM_SHARED_VAULT',
+      createdAt: this.timer.getUTCDate(),
+      meta: {
+        correlation: {
+          userIdentifier: dto.userUuid,
+          userIdentifierType: 'uuid',
+        },
+        origin: DomainEventService.SyncingServer,
+      },
+      payload: dto,
+    }
+  }
+  createUserAddedToSharedVaultEvent(dto: {
+    sharedVaultUuid: string
+    userUuid: string
+    permission: string
+    createdAt: number
+    updatedAt: number
+  }): UserAddedToSharedVaultEvent {
+    return {
+      type: 'USER_ADDED_TO_SHARED_VAULT',
+      createdAt: this.timer.getUTCDate(),
+      meta: {
+        correlation: {
+          userIdentifier: dto.userUuid,
+          userIdentifierType: 'uuid',
+        },
+        origin: DomainEventService.SyncingServer,
+      },
+      payload: dto,
+    }
+  }
+
   createUserInvitedToSharedVaultEvent(dto: {
     invite: {
       uuid: string

+ 13 - 0
packages/syncing-server/src/Domain/Event/DomainEventFactoryInterface.ts

@@ -7,7 +7,9 @@ import {
   NotificationAddedForUserEvent,
   RevisionsCopyRequestedEvent,
   TransitionStatusUpdatedEvent,
+  UserAddedToSharedVaultEvent,
   UserInvitedToSharedVaultEvent,
+  UserRemovedFromSharedVaultEvent,
   WebSocketMessageRequestedEvent,
 } from '@standardnotes/domain-events'
 
@@ -80,4 +82,15 @@ export interface DomainEventFactoryInterface {
     userUuid: string,
     dto: { originalItemUuid: string; newItemUuid: string; roleNames: string[] },
   ): RevisionsCopyRequestedEvent
+  createUserAddedToSharedVaultEvent(dto: {
+    sharedVaultUuid: string
+    userUuid: string
+    permission: string
+    createdAt: number
+    updatedAt: number
+  }): UserAddedToSharedVaultEvent
+  createUserRemovedFromSharedVaultEvent(dto: {
+    sharedVaultUuid: string
+    userUuid: string
+  }): UserRemovedFromSharedVaultEvent
 }

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

@@ -6,8 +6,8 @@ import {
   Timestamps,
   UniqueEntityId,
   Uuid,
+  SharedVaultUser,
 } from '@standardnotes/domain-core'
-import { SharedVaultUser } from '../../SharedVault/User/SharedVaultUser'
 import { SharedVaultUserRepositoryInterface } from '../../SharedVault/User/SharedVaultUserRepositoryInterface'
 import { DetermineSharedVaultOperationOnItem } from '../../UseCase/SharedVaults/DetermineSharedVaultOperationOnItem/DetermineSharedVaultOperationOnItem'
 import { SharedVaultFilter } from './SharedVaultFilter'

+ 0 - 8
packages/syncing-server/src/Domain/SharedVault/User/SharedVaultUserProps.ts

@@ -1,8 +0,0 @@
-import { SharedVaultUserPermission, Timestamps, Uuid } from '@standardnotes/domain-core'
-
-export interface SharedVaultUserProps {
-  sharedVaultUuid: Uuid
-  userUuid: Uuid
-  permission: SharedVaultUserPermission
-  timestamps: Timestamps
-}

+ 1 - 3
packages/syncing-server/src/Domain/SharedVault/User/SharedVaultUserRepositoryInterface.ts

@@ -1,6 +1,4 @@
-import { Uuid } from '@standardnotes/domain-core'
-
-import { SharedVaultUser } from './SharedVaultUser'
+import { SharedVaultUser, Uuid } from '@standardnotes/domain-core'
 
 export interface SharedVaultUserRepositoryInterface {
   findByUuid(sharedVaultUserUuid: Uuid): Promise<SharedVaultUser | null>

+ 1 - 1
packages/syncing-server/src/Domain/UseCase/Messaging/AddNotificationsForUsers/AddNotificationsForUsers.spec.ts

@@ -5,8 +5,8 @@ import {
   Result,
   NotificationPayload,
   NotificationType,
+  SharedVaultUser,
 } from '@standardnotes/domain-core'
-import { SharedVaultUser } from '../../../SharedVault/User/SharedVaultUser'
 import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface'
 import { AddNotificationForUser } from '../AddNotificationForUser/AddNotificationForUser'
 import { AddNotificationsForUsers } from './AddNotificationsForUsers'

+ 22 - 3
packages/syncing-server/src/Domain/UseCase/SharedVaults/AddUserToSharedVault/AddUserToSharedVault.spec.ts

@@ -1,20 +1,31 @@
 import { TimerInterface } from '@standardnotes/time'
+import { Result, SharedVaultUser } from '@standardnotes/domain-core'
+import { DomainEventInterface, DomainEventPublisherInterface } from '@standardnotes/domain-events'
+
 import { SharedVaultRepositoryInterface } from '../../../SharedVault/SharedVaultRepositoryInterface'
 import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface'
 import { AddUserToSharedVault } from './AddUserToSharedVault'
-import { Result } from '@standardnotes/domain-core'
 import { SharedVault } from '../../../SharedVault/SharedVault'
-import { SharedVaultUser } from '../../../SharedVault/User/SharedVaultUser'
+import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface'
 
 describe('AddUserToSharedVault', () => {
   let sharedVaultRepository: SharedVaultRepositoryInterface
   let sharedVaultUserRepository: SharedVaultUserRepositoryInterface
   let timer: TimerInterface
   let sharedVault: SharedVault
+  let domainEventFactory: DomainEventFactoryInterface
+  let domainEventPublisher: DomainEventPublisherInterface
 
   const validUuid = '00000000-0000-0000-0000-000000000000'
 
-  const createUseCase = () => new AddUserToSharedVault(sharedVaultRepository, sharedVaultUserRepository, timer)
+  const createUseCase = () =>
+    new AddUserToSharedVault(
+      sharedVaultRepository,
+      sharedVaultUserRepository,
+      timer,
+      domainEventFactory,
+      domainEventPublisher,
+    )
 
   beforeEach(() => {
     sharedVault = {} as jest.Mocked<SharedVault>
@@ -27,6 +38,14 @@ describe('AddUserToSharedVault', () => {
 
     timer = {} as jest.Mocked<TimerInterface>
     timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(123456789)
+
+    domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
+    domainEventFactory.createUserAddedToSharedVaultEvent = jest
+      .fn()
+      .mockReturnValue({} as jest.Mocked<DomainEventInterface>)
+
+    domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
+    domainEventPublisher.publish = jest.fn()
   })
 
   it('should return a failure result if the shared vault uuid is invalid', async () => {

+ 22 - 2
packages/syncing-server/src/Domain/UseCase/SharedVaults/AddUserToSharedVault/AddUserToSharedVault.ts

@@ -1,16 +1,26 @@
-import { Result, SharedVaultUserPermission, Timestamps, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
+import {
+  Result,
+  SharedVaultUser,
+  SharedVaultUserPermission,
+  Timestamps,
+  UseCaseInterface,
+  Uuid,
+} from '@standardnotes/domain-core'
 import { TimerInterface } from '@standardnotes/time'
+import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
 
 import { AddUserToSharedVaultDTO } from './AddUserToSharedVaultDTO'
 import { SharedVaultRepositoryInterface } from '../../../SharedVault/SharedVaultRepositoryInterface'
-import { SharedVaultUser } from '../../../SharedVault/User/SharedVaultUser'
 import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface'
+import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface'
 
 export class AddUserToSharedVault implements UseCaseInterface<SharedVaultUser> {
   constructor(
     private sharedVaultRepository: SharedVaultRepositoryInterface,
     private sharedVaultUserRepository: SharedVaultUserRepositoryInterface,
     private timer: TimerInterface,
+    private domainEventFactory: DomainEventFactoryInterface,
+    private domainEventPublisher: DomainEventPublisherInterface,
   ) {}
 
   async execute(dto: AddUserToSharedVaultDTO): Promise<Result<SharedVaultUser>> {
@@ -57,6 +67,16 @@ export class AddUserToSharedVault implements UseCaseInterface<SharedVaultUser> {
 
     await this.sharedVaultUserRepository.save(sharedVaultUser)
 
+    await this.domainEventPublisher.publish(
+      this.domainEventFactory.createUserAddedToSharedVaultEvent({
+        sharedVaultUuid: sharedVaultUser.props.sharedVaultUuid.value,
+        userUuid: sharedVaultUser.props.userUuid.value,
+        permission: sharedVaultUser.props.permission.value,
+        createdAt: sharedVaultUser.props.timestamps.createdAt,
+        updatedAt: sharedVaultUser.props.timestamps.updatedAt,
+      }),
+    )
+
     return Result.ok(sharedVaultUser)
   }
 }

+ 2 - 1
packages/syncing-server/src/Domain/UseCase/SharedVaults/CreateSharedVault/CreateSharedVaultResult.ts

@@ -1,5 +1,6 @@
+import { SharedVaultUser } from '@standardnotes/domain-core'
+
 import { SharedVault } from '../../../SharedVault/SharedVault'
-import { SharedVaultUser } from '../../../SharedVault/User/SharedVaultUser'
 
 export interface CreateSharedVaultResult {
   sharedVaultUser: SharedVaultUser

+ 1 - 2
packages/syncing-server/src/Domain/UseCase/SharedVaults/CreateSharedVaultFileValetToken/CreateSharedVaultFileValetToken.spec.ts

@@ -3,8 +3,7 @@ import { SharedVaultRepositoryInterface } from '../../../SharedVault/SharedVault
 import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface'
 import { CreateSharedVaultFileValetToken } from './CreateSharedVaultFileValetToken'
 import { SharedVault } from '../../../SharedVault/SharedVault'
-import { SharedVaultUser } from '../../../SharedVault/User/SharedVaultUser'
-import { SharedVaultUserPermission, Timestamps, Uuid } from '@standardnotes/domain-core'
+import { SharedVaultUser, SharedVaultUserPermission, Timestamps, Uuid } from '@standardnotes/domain-core'
 
 describe('CreateSharedVaultFileValetToken', () => {
   let sharedVaultRepository: SharedVaultRepositoryInterface

+ 1 - 2
packages/syncing-server/src/Domain/UseCase/SharedVaults/DeleteSharedVault/DeleteSharedVault.spec.ts

@@ -1,11 +1,10 @@
-import { Uuid, Timestamps, Result, SharedVaultUserPermission } from '@standardnotes/domain-core'
+import { Uuid, Timestamps, Result, SharedVaultUserPermission, SharedVaultUser } from '@standardnotes/domain-core'
 
 import { SharedVaultRepositoryInterface } from '../../../SharedVault/SharedVaultRepositoryInterface'
 import { SharedVaultInviteRepositoryInterface } from '../../../SharedVault/User/Invite/SharedVaultInviteRepositoryInterface'
 import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface'
 import { DeleteSharedVault } from './DeleteSharedVault'
 import { SharedVault } from '../../../SharedVault/SharedVault'
-import { SharedVaultUser } from '../../../SharedVault/User/SharedVaultUser'
 import { RemoveUserFromSharedVault } from '../RemoveUserFromSharedVault/RemoveUserFromSharedVault'
 
 describe('DeleteSharedVault', () => {

+ 2 - 2
packages/syncing-server/src/Domain/UseCase/SharedVaults/GetSharedVaultUsers/GetSharedVaultUsers.spec.ts

@@ -1,7 +1,7 @@
-import { Uuid, Timestamps, SharedVaultUserPermission } from '@standardnotes/domain-core'
+import { Uuid, Timestamps, SharedVaultUserPermission, SharedVaultUser } from '@standardnotes/domain-core'
+
 import { SharedVault } from '../../../SharedVault/SharedVault'
 import { SharedVaultRepositoryInterface } from '../../../SharedVault/SharedVaultRepositoryInterface'
-import { SharedVaultUser } from '../../../SharedVault/User/SharedVaultUser'
 import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface'
 import { GetSharedVaultUsers } from './GetSharedVaultUsers'
 

+ 2 - 2
packages/syncing-server/src/Domain/UseCase/SharedVaults/GetSharedVaultUsers/GetSharedVaultUsers.ts

@@ -1,5 +1,5 @@
-import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
-import { SharedVaultUser } from '../../../SharedVault/User/SharedVaultUser'
+import { Result, SharedVaultUser, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
+
 import { GetSharedVaultUsersDTO } from './GetSharedVaultUsersDTO'
 import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface'
 import { SharedVaultRepositoryInterface } from '../../../SharedVault/SharedVaultRepositoryInterface'

+ 2 - 2
packages/syncing-server/src/Domain/UseCase/SharedVaults/GetSharedVaults/GetSharedVaults.spec.ts

@@ -1,7 +1,7 @@
-import { SharedVaultUserPermission, Timestamps, Uuid } from '@standardnotes/domain-core'
+import { SharedVaultUser, SharedVaultUserPermission, Timestamps, Uuid } from '@standardnotes/domain-core'
+
 import { SharedVault } from '../../../SharedVault/SharedVault'
 import { SharedVaultRepositoryInterface } from '../../../SharedVault/SharedVaultRepositoryInterface'
-import { SharedVaultUser } from '../../../SharedVault/User/SharedVaultUser'
 import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface'
 import { GetSharedVaults } from './GetSharedVaults'
 

+ 2 - 3
packages/syncing-server/src/Domain/UseCase/SharedVaults/InviteUserToSharedVault/InviteUserToSharedVault.spec.ts

@@ -1,5 +1,6 @@
 import { TimerInterface } from '@standardnotes/time'
-import { Uuid, Timestamps, Result, SharedVaultUserPermission } from '@standardnotes/domain-core'
+import { Uuid, Timestamps, Result, SharedVaultUserPermission, SharedVaultUser } from '@standardnotes/domain-core'
+import { UserInvitedToSharedVaultEvent } from '@standardnotes/domain-events'
 import { Logger } from 'winston'
 
 import { SharedVaultRepositoryInterface } from '../../../SharedVault/SharedVaultRepositoryInterface'
@@ -8,10 +9,8 @@ import { InviteUserToSharedVault } from './InviteUserToSharedVault'
 import { SharedVault } from '../../../SharedVault/SharedVault'
 import { SharedVaultInvite } from '../../../SharedVault/User/Invite/SharedVaultInvite'
 import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface'
-import { SharedVaultUser } from '../../../SharedVault/User/SharedVaultUser'
 import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface'
 import { SendEventToClient } from '../../Syncing/SendEventToClient/SendEventToClient'
-import { UserInvitedToSharedVaultEvent } from '@standardnotes/domain-events'
 
 describe('InviteUserToSharedVault', () => {
   let sharedVaultRepository: SharedVaultRepositoryInterface

+ 27 - 3
packages/syncing-server/src/Domain/UseCase/SharedVaults/RemoveUserFromSharedVault/RemoveUserFromSharedVault.spec.ts

@@ -1,11 +1,19 @@
-import { Uuid, Timestamps, Result, NotificationPayload, SharedVaultUserPermission } from '@standardnotes/domain-core'
+import {
+  Uuid,
+  Timestamps,
+  Result,
+  NotificationPayload,
+  SharedVaultUserPermission,
+  SharedVaultUser,
+} from '@standardnotes/domain-core'
 
 import { SharedVault } from '../../../SharedVault/SharedVault'
 import { SharedVaultRepositoryInterface } from '../../../SharedVault/SharedVaultRepositoryInterface'
-import { SharedVaultUser } from '../../../SharedVault/User/SharedVaultUser'
 import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface'
 import { RemoveUserFromSharedVault } from './RemoveUserFromSharedVault'
 import { AddNotificationForUser } from '../../Messaging/AddNotificationForUser/AddNotificationForUser'
+import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface'
+import { DomainEventInterface, DomainEventPublisherInterface } from '@standardnotes/domain-events'
 
 describe('RemoveUserFromSharedVault', () => {
   let sharedVaultRepository: SharedVaultRepositoryInterface
@@ -13,9 +21,17 @@ describe('RemoveUserFromSharedVault', () => {
   let addNotificationForUser: AddNotificationForUser
   let sharedVault: SharedVault
   let sharedVaultUser: SharedVaultUser
+  let domainEventFactory: DomainEventFactoryInterface
+  let domainEventPublisher: DomainEventPublisherInterface
 
   const createUseCase = () =>
-    new RemoveUserFromSharedVault(sharedVaultUserRepository, sharedVaultRepository, addNotificationForUser)
+    new RemoveUserFromSharedVault(
+      sharedVaultUserRepository,
+      sharedVaultRepository,
+      addNotificationForUser,
+      domainEventFactory,
+      domainEventPublisher,
+    )
 
   beforeEach(() => {
     sharedVault = SharedVault.create({
@@ -39,6 +55,14 @@ describe('RemoveUserFromSharedVault', () => {
 
     addNotificationForUser = {} as jest.Mocked<AddNotificationForUser>
     addNotificationForUser.execute = jest.fn().mockReturnValue(Result.ok())
+
+    domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
+    domainEventFactory.createUserRemovedFromSharedVaultEvent = jest
+      .fn()
+      .mockReturnValue({} as jest.Mocked<DomainEventInterface>)
+
+    domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
+    domainEventPublisher.publish = jest.fn()
   })
 
   it('should remove user from shared vault', async () => {

+ 11 - 0
packages/syncing-server/src/Domain/UseCase/SharedVaults/RemoveUserFromSharedVault/RemoveUserFromSharedVault.ts

@@ -1,15 +1,19 @@
 import { NotificationPayload, NotificationType, Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core'
+import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
 
 import { RemoveUserFromSharedVaultDTO } from './RemoveUserFromSharedVaultDTO'
 import { SharedVaultRepositoryInterface } from '../../../SharedVault/SharedVaultRepositoryInterface'
 import { SharedVaultUserRepositoryInterface } from '../../../SharedVault/User/SharedVaultUserRepositoryInterface'
 import { AddNotificationForUser } from '../../Messaging/AddNotificationForUser/AddNotificationForUser'
+import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface'
 
 export class RemoveUserFromSharedVault implements UseCaseInterface<void> {
   constructor(
     private sharedVaultUsersRepository: SharedVaultUserRepositoryInterface,
     private sharedVaultRepository: SharedVaultRepositoryInterface,
     private addNotificationForUser: AddNotificationForUser,
+    private domainEventFactory: DomainEventFactoryInterface,
+    private domainEventPublisher: DomainEventPublisherInterface,
   ) {}
 
   async execute(dto: RemoveUserFromSharedVaultDTO): Promise<Result<void>> {
@@ -77,6 +81,13 @@ export class RemoveUserFromSharedVault implements UseCaseInterface<void> {
       return Result.fail(result.getError())
     }
 
+    await this.domainEventPublisher.publish(
+      this.domainEventFactory.createUserRemovedFromSharedVaultEvent({
+        sharedVaultUuid: dto.sharedVaultUuid,
+        userUuid: dto.userUuid,
+      }),
+    )
+
     return Result.ok()
   }
 }

+ 1 - 2
packages/syncing-server/src/Infra/InversifyExpressUtils/AnnotatedSharedVaultUsersController.ts

@@ -1,11 +1,10 @@
 import { controller, httpDelete, httpGet, results } from 'inversify-express-utils'
 import { inject } from 'inversify'
-import { MapperInterface } from '@standardnotes/domain-core'
+import { MapperInterface, SharedVaultUser } from '@standardnotes/domain-core'
 import { Request, Response } from 'express'
 
 import { BaseSharedVaultUsersController } from './Base/BaseSharedVaultUsersController'
 import TYPES from '../../Bootstrap/Types'
-import { SharedVaultUser } from '../../Domain/SharedVault/User/SharedVaultUser'
 import { SharedVaultUserHttpRepresentation } from '../../Mapping/Http/SharedVaultUserHttpRepresentation'
 import { GetSharedVaultUsers } from '../../Domain/UseCase/SharedVaults/GetSharedVaultUsers/GetSharedVaultUsers'
 import { RemoveUserFromSharedVault } from '../../Domain/UseCase/SharedVaults/RemoveUserFromSharedVault/RemoveUserFromSharedVault'

+ 1 - 2
packages/syncing-server/src/Infra/InversifyExpressUtils/AnnotatedSharedVaultsController.ts

@@ -1,12 +1,11 @@
 import { controller, httpDelete, httpGet, httpPost, results } from 'inversify-express-utils'
 import { inject } from 'inversify'
-import { MapperInterface } from '@standardnotes/domain-core'
+import { MapperInterface, SharedVaultUser } from '@standardnotes/domain-core'
 import { Request, Response } from 'express'
 
 import { BaseSharedVaultsController } from './Base/BaseSharedVaultsController'
 import TYPES from '../../Bootstrap/Types'
 import { SharedVault } from '../../Domain/SharedVault/SharedVault'
-import { SharedVaultUser } from '../../Domain/SharedVault/User/SharedVaultUser'
 import { CreateSharedVault } from '../../Domain/UseCase/SharedVaults/CreateSharedVault/CreateSharedVault'
 import { CreateSharedVaultFileValetToken } from '../../Domain/UseCase/SharedVaults/CreateSharedVaultFileValetToken/CreateSharedVaultFileValetToken'
 import { DeleteSharedVault } from '../../Domain/UseCase/SharedVaults/DeleteSharedVault/DeleteSharedVault'

+ 2 - 0
packages/syncing-server/src/Infra/InversifyExpressUtils/Base/BaseSharedVaultInvitesController.ts

@@ -128,6 +128,8 @@ export class BaseSharedVaultInvitesController extends BaseHttpController {
       )
     }
 
+    response.setHeader('x-invalidate-cache', response.locals.user.uuid)
+
     return this.json({
       success: true,
     })

+ 3 - 2
packages/syncing-server/src/Infra/InversifyExpressUtils/Base/BaseSharedVaultUsersController.ts

@@ -1,9 +1,8 @@
 import { Request, Response } from 'express'
 import { BaseHttpController, results } from 'inversify-express-utils'
 import { HttpStatusCode } from '@standardnotes/responses'
-import { ControllerContainerInterface, MapperInterface } from '@standardnotes/domain-core'
+import { ControllerContainerInterface, MapperInterface, SharedVaultUser } from '@standardnotes/domain-core'
 
-import { SharedVaultUser } from '../../../Domain/SharedVault/User/SharedVaultUser'
 import { SharedVaultUserHttpRepresentation } from '../../../Mapping/Http/SharedVaultUserHttpRepresentation'
 import { GetSharedVaultUsers } from '../../../Domain/UseCase/SharedVaults/GetSharedVaultUsers/GetSharedVaultUsers'
 import { RemoveUserFromSharedVault } from '../../../Domain/UseCase/SharedVaults/RemoveUserFromSharedVault/RemoveUserFromSharedVault'
@@ -66,6 +65,8 @@ export class BaseSharedVaultUsersController extends BaseHttpController {
       )
     }
 
+    response.setHeader('x-invalidate-cache', response.locals.user.uuid)
+
     return this.json({
       success: true,
     })

+ 5 - 2
packages/syncing-server/src/Infra/InversifyExpressUtils/Base/BaseSharedVaultsController.ts

@@ -1,14 +1,13 @@
 import { Request, Response } from 'express'
 import { BaseHttpController, results } from 'inversify-express-utils'
 import { HttpStatusCode } from '@standardnotes/responses'
-import { ControllerContainerInterface, MapperInterface } from '@standardnotes/domain-core'
+import { ControllerContainerInterface, MapperInterface, SharedVaultUser } from '@standardnotes/domain-core'
 import { Role } from '@standardnotes/security'
 
 import { GetSharedVaults } from '../../../Domain/UseCase/SharedVaults/GetSharedVaults/GetSharedVaults'
 import { SharedVault } from '../../../Domain/SharedVault/SharedVault'
 import { SharedVaultHttpRepresentation } from '../../../Mapping/Http/SharedVaultHttpRepresentation'
 import { CreateSharedVault } from '../../../Domain/UseCase/SharedVaults/CreateSharedVault/CreateSharedVault'
-import { SharedVaultUser } from '../../../Domain/SharedVault/User/SharedVaultUser'
 import { SharedVaultUserHttpRepresentation } from '../../../Mapping/Http/SharedVaultUserHttpRepresentation'
 import { DeleteSharedVault } from '../../../Domain/UseCase/SharedVaults/DeleteSharedVault/DeleteSharedVault'
 import { CreateSharedVaultFileValetToken } from '../../../Domain/UseCase/SharedVaults/CreateSharedVaultFileValetToken/CreateSharedVaultFileValetToken'
@@ -74,6 +73,8 @@ export class BaseSharedVaultsController extends BaseHttpController {
       )
     }
 
+    response.setHeader('x-invalidate-cache', response.locals.user.uuid)
+
     return this.json({
       sharedVault: this.sharedVaultHttpMapper.toProjection(result.getValue().sharedVault),
       sharedVaultUser: this.sharedVaultUserHttpMapper.toProjection(result.getValue().sharedVaultUser),
@@ -97,6 +98,8 @@ export class BaseSharedVaultsController extends BaseHttpController {
       )
     }
 
+    response.setHeader('x-invalidate-cache', response.locals.user.uuid)
+
     return this.json({ success: true })
   }
 

+ 1 - 2
packages/syncing-server/src/Infra/TypeORM/TypeORMSharedVaultUserRepository.ts

@@ -1,8 +1,7 @@
 import { Repository } from 'typeorm'
-import { MapperInterface, Uuid } from '@standardnotes/domain-core'
+import { MapperInterface, SharedVaultUser, Uuid } from '@standardnotes/domain-core'
 
 import { TypeORMSharedVaultUser } from './TypeORMSharedVaultUser'
-import { SharedVaultUser } from '../../Domain/SharedVault/User/SharedVaultUser'
 import { SharedVaultUserRepositoryInterface } from '../../Domain/SharedVault/User/SharedVaultUserRepositoryInterface'
 
 export class TypeORMSharedVaultUserRepository implements SharedVaultUserRepositoryInterface {

+ 9 - 0
packages/syncing-server/src/Mapping/Backup/ItemBackupMapper.ts

@@ -27,6 +27,15 @@ export class ItemBackupMapper implements MapperInterface<Item, ItemBackupReprese
       updated_at_timestamp: domain.props.timestamps.updatedAt,
       updated_with_session: domain.props.updatedWithSession ? domain.props.updatedWithSession.value : null,
       user_uuid: domain.props.userUuid.value,
+      key_system_identifier: domain.props.keySystemAssociation
+        ? domain.props.keySystemAssociation.props.keySystemIdentifier
+        : null,
+      shared_vault_uuid: domain.props.sharedVaultAssociation
+        ? domain.props.sharedVaultAssociation.props.sharedVaultUuid.value
+        : null,
+      last_edited_by: domain.props.sharedVaultAssociation
+        ? domain.props.sharedVaultAssociation.props.lastEditedBy.value
+        : null,
     }
   }
 }

+ 3 - 0
packages/syncing-server/src/Mapping/Backup/ItemBackupRepresentation.ts

@@ -13,4 +13,7 @@ export interface ItemBackupRepresentation {
   updated_at_timestamp: number
   updated_with_session: string | null
   user_uuid: string
+  key_system_identifier: string | null
+  shared_vault_uuid: string | null
+  last_edited_by: string | null
 }

+ 1 - 2
packages/syncing-server/src/Mapping/Http/SharedVaultUserHttpMapper.ts

@@ -1,6 +1,5 @@
-import { MapperInterface } from '@standardnotes/domain-core'
+import { MapperInterface, SharedVaultUser } from '@standardnotes/domain-core'
 
-import { SharedVaultUser } from '../../Domain/SharedVault/User/SharedVaultUser'
 import { SharedVaultUserHttpRepresentation } from './SharedVaultUserHttpRepresentation'
 
 export class SharedVaultUserHttpMapper implements MapperInterface<SharedVaultUser, SharedVaultUserHttpRepresentation> {

+ 1 - 1
packages/syncing-server/src/Mapping/Persistence/SharedVaultUserPersistenceMapper.ts

@@ -4,9 +4,9 @@ import {
   UniqueEntityId,
   Uuid,
   SharedVaultUserPermission,
+  SharedVaultUser,
 } from '@standardnotes/domain-core'
 
-import { SharedVaultUser } from '../../Domain/SharedVault/User/SharedVaultUser'
 import { TypeORMSharedVaultUser } from '../../Infra/TypeORM/TypeORMSharedVaultUser'
 
 export class SharedVaultUserPersistenceMapper implements MapperInterface<SharedVaultUser, TypeORMSharedVaultUser> {