Compare commits

...
92 changed files with 1549 additions and 827 deletions
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.1.111](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-24)
**Note:** Version bump only for package @standardnotes/clipper
## [1.1.110](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-24)
**Note:** Version bump only for package @standardnotes/clipper
## [1.1.109](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-24)
**Note:** Version bump only for package @standardnotes/clipper
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@standardnotes/clipper",
"description": "Web clipper browser extension for Standard Notes",
"version": "1.1.109",
"version": "1.1.111",
"private": true,
"scripts": {
"build-mv2": "yarn clean && webpack --config ./webpack.config.prod.js",
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.108.42](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-24)
**Note:** Version bump only for package @standardnotes/desktop
## [3.108.41](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-24)
**Note:** Version bump only for package @standardnotes/desktop
## [3.108.40](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-24)
**Note:** Version bump only for package @standardnotes/desktop
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@standardnotes/desktop",
"main": "./app/dist/index.js",
"version": "3.108.40",
"version": "3.108.42",
"license": "AGPL-3.0-or-later",
"author": "Standard Notes.",
"private": true,
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.56.21](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-24)
**Note:** Version bump only for package @standardnotes/mobile
## [3.56.20](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-24)
**Note:** Version bump only for package @standardnotes/mobile
## [3.56.19](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-24)
**Note:** Version bump only for package @standardnotes/mobile
@@ -118,7 +118,24 @@ class ReceiveSharingIntent: NSObject {
@objc
func clearFileNames(){
print("clearFileNames");
let appDomain = Bundle.main.bundleIdentifier!
let groupName = "group.\(appDomain)"
let userDefaults = UserDefaults(suiteName: groupName)
userDefaults?.set(nil, forKey: "ShareKey.media")
userDefaults?.set(nil, forKey: "ShareKey.text")
userDefaults?.set(nil, forKey: "ShareKey.url")
let containerURL = FileManager.default
.containerURL(forSecurityApplicationGroupIdentifier: groupName)!
let shareTempDirPath = containerURL.appendingPathComponent("ShareTemp", isDirectory: true)
do {
let fileNames = try FileManager.default.contentsOfDirectory(atPath: shareTempDirPath.relativePath)
for fileName in fileNames {
let filePath = shareTempDirPath.appendingPathComponent(fileName)
try FileManager.default.removeItem(at: filePath)
}
} catch {
print(error)
}
}
@@ -95,16 +95,11 @@ class ShareViewController: SLComposeServiceViewController {
attachment.loadItem(forTypeIdentifier: imageContentType, options: nil) { [weak self] data, error in
if error == nil, let url = data as? URL, let this = self {
// this.redirectToHostApp(type: .media)
// Always copy
let fileExtension = this.getExtension(from: url, type: .video)
let newName = UUID().uuidString
let newPath = FileManager.default
.containerURL(forSecurityApplicationGroupIdentifier: "group.\(hostAppBundleIdentifier)")!
.appendingPathComponent("\(newName).\(fileExtension)")
let copied = this.copyFile(at: url, to: newPath)
if(copied) {
this.sharedMedia.append(SharedMediaFile(path: newPath.absoluteString, thumbnail: nil, duration: nil, type: .image))
if let copiedPath = this.copyToTemporaryPath(url: url, name: "\(newName).\(fileExtension)") {
this.sharedMedia.append(SharedMediaFile(path: copiedPath, thumbnail: nil, duration: nil, type: .image))
}
if index == (content.attachments?.count)! - 1 {
@@ -125,15 +120,8 @@ class ShareViewController: SLComposeServiceViewController {
// Always copy
let fileExtension = this.getExtension(from: url, type: .video)
let newName = UUID().uuidString
let newPath = FileManager.default
.containerURL(forSecurityApplicationGroupIdentifier: "group.\(hostAppBundleIdentifier)")!
.appendingPathComponent("\(newName).\(fileExtension)")
let copied = this.copyFile(at: url, to: newPath)
if(copied) {
guard let sharedFile = this.getSharedMediaFile(forVideo: newPath) else {
return
}
this.sharedMedia.append(sharedFile)
if let copiedPath = this.copyToTemporaryPath(url: url, name: "\(newName).\(fileExtension)") {
this.sharedMedia.append(SharedMediaFile(path: copiedPath, thumbnail: nil, duration: nil, type: .video))
}
if index == (content.attachments?.count)! - 1 {
@@ -151,14 +139,9 @@ class ShareViewController: SLComposeServiceViewController {
if error == nil, let url = data as? URL, let this = self {
// Always copy
let newName = this.getFileName(from :url)
let newPath = FileManager.default
.containerURL(forSecurityApplicationGroupIdentifier: "group.\(hostAppBundleIdentifier)")!
.appendingPathComponent("\(newName)")
let copied = this.copyFile(at: url, to: newPath)
if (copied) {
this.sharedMedia.append(SharedMediaFile(path: newPath.absoluteString, thumbnail: nil, duration: nil, type: .file))
if let copiedPath = this.copyToTemporaryPath(url: url, name: newName) {
this.sharedMedia.append(SharedMediaFile(path: copiedPath, thumbnail: nil, duration: nil, type: .file))
}
if index == (content.attachments?.count)! - 1 {
@@ -171,6 +154,32 @@ class ShareViewController: SLComposeServiceViewController {
}
}
private func directoryExistsAtPath(_ path: String) -> Bool {
var isDirectory = ObjCBool(true)
let exists = FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory)
return exists && isDirectory.boolValue
}
private func copyToTemporaryPath(url: URL, name: String) -> String? {
let containerURL = FileManager.default
.containerURL(forSecurityApplicationGroupIdentifier: "group.\(hostAppBundleIdentifier)")!
let shareTempDirPath = containerURL.appendingPathComponent("ShareTemp", isDirectory: true)
if !directoryExistsAtPath(shareTempDirPath.absoluteString) {
do {
try FileManager.default.createDirectory(atPath: shareTempDirPath.relativePath, withIntermediateDirectories: true, attributes: nil)
} catch {
return nil
}
}
let newPath = shareTempDirPath
.appendingPathComponent("\(name)")
let copied = self.copyFile(at: url, to: newPath)
if (copied) {
return newPath.absoluteString
}
return nil
}
private func redirectToHostApp() {
let userDefaults = UserDefaults(suiteName: "group.\(hostAppBundleIdentifier)")
userDefaults?.set(self.toData(data: self.sharedMedia), forKey: "\(self.sharedKey).media")
@@ -263,40 +272,6 @@ class ShareViewController: SLComposeServiceViewController {
return true
}
private func getSharedMediaFile(forVideo: URL) -> SharedMediaFile? {
let asset = AVAsset(url: forVideo)
let duration = (CMTimeGetSeconds(asset.duration) * 1000).rounded()
let thumbnailPath = getThumbnailPath(for: forVideo)
if FileManager.default.fileExists(atPath: thumbnailPath.path) {
return SharedMediaFile(path: forVideo.absoluteString, thumbnail: thumbnailPath.absoluteString, duration: duration, type: .video)
}
var saved = false
let assetImgGenerate = AVAssetImageGenerator(asset: asset)
assetImgGenerate.appliesPreferredTrackTransform = true
// let scale = UIScreen.main.scale
assetImgGenerate.maximumSize = CGSize(width: 360, height: 360)
do {
let img = try assetImgGenerate.copyCGImage(at: CMTimeMakeWithSeconds(600, preferredTimescale: Int32(1.0)), actualTime: nil)
try UIImage.pngData(UIImage(cgImage: img))()?.write(to: thumbnailPath)
saved = true
} catch {
saved = false
}
return saved ? SharedMediaFile(path: forVideo.absoluteString, thumbnail: thumbnailPath.absoluteString, duration: duration, type: .video) : nil
}
private func getThumbnailPath(for url: URL) -> URL {
let fileName = Data(url.lastPathComponent.utf8).base64EncodedString().replacingOccurrences(of: "==", with: "")
let path = FileManager.default
.containerURL(forSecurityApplicationGroupIdentifier: "group.\(hostAppBundleIdentifier)")!
.appendingPathComponent("\(fileName).jpg")
return path
}
class SharedMediaFile: Codable {
var path: String; // can be image, video or url path. It can also be text content
var thumbnail: String?; // video thumbnail
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/mobile",
"version": "3.56.19",
"version": "3.56.21",
"author": "Standard Notes.",
"private": true,
"license": "AGPL-3.0-or-later",
@@ -71,6 +71,7 @@ export class ReceivedSharedItemsHandler {
}
deinit() {
ReceiveSharingIntent.clearFileNames()
this.receivedItemsQueue = []
this.eventSub?.remove()
}
@@ -100,17 +101,13 @@ export class ReceivedSharedItemsHandler {
}
handleItemsQueue = async () => {
if (!this.receivedItemsQueue.length) {
return
}
const item = this.receivedItemsQueue.shift()
if (!item) {
return
}
if (isReceivedAndroidFile(item) || isReceivedIosFile(item)) {
this.sendFileToWebView(item).catch(console.error)
await this.sendFileToWebView(item).catch(console.error)
} else if (isReceivedWeblink(item)) {
this.webViewRef.current?.postMessage(
JSON.stringify({
@@ -134,6 +131,14 @@ export class ReceivedSharedItemsHandler {
)
}
if (!this.receivedItemsQueue.length) {
if (Platform.OS === 'ios') {
ReceiveSharingIntent.clearFileNames()
}
return
}
this.handleItemsQueue().catch(console.error)
}
@@ -157,7 +162,12 @@ export class ReceivedSharedItemsHandler {
private addSharedItemsToQueue = async (url?: string) => {
const received =
Platform.OS === 'ios' ? await ReceiveSharingIntent.getFileNames(url) : await ReceiveSharingIntent.getFileNames()
ReceiveSharingIntent.clearFileNames()
// On iOS, we need to wait for the whole queue
// to finish before clearing
if (Platform.OS === 'android') {
ReceiveSharingIntent.clearFileNames()
}
if (!received) {
return
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.4.393](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-24)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.392](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-24)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.391](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-24)
**Note:** Version bump only for package @standardnotes/releases
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/releases",
"version": "1.4.391",
"version": "1.4.393",
"license": "AGPL-3.0-or-later",
"main": "dist/releases.json",
"types": "dist/index.d.ts",
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.63.8](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-24)
**Note:** Version bump only for package @standardnotes/services
## [1.63.7](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-24)
**Note:** Version bump only for package @standardnotes/services
## [1.63.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-23)
**Note:** Version bump only for package @standardnotes/services
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/services",
"version": "1.63.6",
"version": "1.63.8",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
@@ -1,3 +1,4 @@
import { SessionsClientInterface } from './../Session/SessionsClientInterface'
import { EncryptionProviderInterface } from './../Encryption/EncryptionProviderInterface'
import { GetUntrustedPayload } from './UseCase/GetUntrustedPayload'
import { GetInboundMessages } from './UseCase/GetInboundMessages'
@@ -31,6 +32,7 @@ describe('AsymmetricMessageService', () => {
let sync: jest.Mocked<SyncServiceInterface>
let mutator: jest.Mocked<MutatorClientInterface>
let encryption: jest.Mocked<EncryptionProviderInterface>
let sessions: jest.Mocked<SessionsClientInterface>
let service: AsymmetricMessageService
beforeEach(() => {
@@ -60,9 +62,10 @@ describe('AsymmetricMessageService', () => {
eventBus.addEventHandler = jest.fn()
service = new AsymmetricMessageService(
messageServer,
encryption,
mutator,
sessions,
messageServer,
createOrEditContact,
findContact,
getAllContacts,
@@ -1,3 +1,4 @@
import { SessionsClientInterface } from './../Session/SessionsClientInterface'
import { MutatorClientInterface } from './../Mutator/MutatorClientInterface'
import { AsymmetricMessageServerHash, ClientDisplayableError, isClientDisplayableError } from '@standardnotes/responses'
import { SyncEvent, SyncEventReceivedAsymmetricMessagesData } from '../Event/SyncEvent'
@@ -39,9 +40,10 @@ export class AsymmetricMessageService
implements AsymmetricMessageServiceInterface, InternalEventHandlerInterface
{
constructor(
private messageServer: AsymmetricMessageServer,
private encryption: EncryptionProviderInterface,
private mutator: MutatorClientInterface,
private sessions: SessionsClientInterface,
private messageServer: AsymmetricMessageServer,
private _createOrEditContact: CreateOrEditContact,
private _findContact: FindContact,
private _getAllContacts: GetAllContacts,
@@ -184,10 +186,6 @@ export class AsymmetricMessageService
message: AsymmetricMessageServerHash,
payload: AsymmetricMessagePayload,
): Promise<void> {
if (payload.data.recipientUuid !== message.user_uuid) {
return
}
if (payload.type === AsymmetricMessagePayloadType.ContactShare) {
await this.handleTrustedContactShareMessage(message, payload)
} else if (payload.type === AsymmetricMessagePayloadType.SenderKeypairChanged) {
@@ -225,6 +223,7 @@ export class AsymmetricMessageService
const result = this._getTrustedPayload.execute({
privateKey: this.encryption.getKeyPair().privateKey,
sender: contact.getValue(),
ownUserUuid: this.sessions.userUuid,
message,
})
@@ -0,0 +1,63 @@
import { AsymmetricMessageServerHash } from '@standardnotes/responses'
import { AsymmetricMessagePayload, TrustedContactInterface } from '@standardnotes/models'
import { DecryptMessage } from '../../Encryption/UseCase/Asymmetric/DecryptMessage'
import { Result } from '@standardnotes/domain-core'
import { GetTrustedPayload } from './GetTrustedPayload'
describe('GetTrustedPayload', () => {
let decryptMessage: jest.Mocked<DecryptMessage>
let getTrustedPayload: GetTrustedPayload
beforeEach(() => {
decryptMessage = {} as jest.Mocked<DecryptMessage>
decryptMessage.execute = jest.fn()
getTrustedPayload = new GetTrustedPayload(decryptMessage)
})
describe('execute', () => {
const mockDto = {
privateKey: 'test-private-key',
message: {} as AsymmetricMessageServerHash,
sender: {} as TrustedContactInterface,
ownUserUuid: 'test-user-uuid',
}
it('should return failure when decryption fails', () => {
decryptMessage.execute = jest.fn().mockReturnValue(Result.fail('Decryption failed'))
const result = getTrustedPayload.execute(mockDto)
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Decryption failed')
})
it('should return failure when recipientUuid is not equal to ownUserUuid', () => {
const mockPayload: AsymmetricMessagePayload = {
data: {
recipientUuid: 'another-user-uuid',
},
} as AsymmetricMessagePayload
decryptMessage.execute = jest.fn().mockReturnValue(Result.ok(mockPayload))
const result = getTrustedPayload.execute(mockDto)
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Message is not for this user')
})
it('should return success when recipientUuid is equal to ownUserUuid', () => {
const mockPayload: AsymmetricMessagePayload = {
data: {
recipientUuid: 'test-user-uuid',
},
} as AsymmetricMessagePayload
decryptMessage.execute = jest.fn().mockReturnValue(Result.ok(mockPayload))
const result = getTrustedPayload.execute(mockDto)
expect(result.isFailed()).toBe(false)
expect(result.getValue()).toBe(mockPayload)
})
})
})
@@ -10,6 +10,7 @@ export class GetTrustedPayload implements SyncUseCaseInterface<AsymmetricMessage
privateKey: string
message: AsymmetricMessageServerHash
sender: TrustedContactInterface
ownUserUuid: string
}): Result<M> {
const result = this.decryptMessage.execute<M>({
message: dto.message.encrypted_message,
@@ -17,6 +18,16 @@ export class GetTrustedPayload implements SyncUseCaseInterface<AsymmetricMessage
privateKey: dto.privateKey,
})
if (result.isFailed()) {
return result
}
const recipientUuid = result.getValue().data.recipientUuid
if (recipientUuid !== dto.ownUserUuid) {
return Result.fail('Message is not for this user')
}
return result
}
}
@@ -1,3 +1,4 @@
import { DeleteContact } from './UseCase/DeleteContact'
import { MutatorClientInterface } from './../Mutator/MutatorClientInterface'
import { UserKeyPairChangedEventData } from './../Session/UserKeyPairChangedEventData'
import { SessionEvent } from './../Session/SessionEvent'
@@ -21,6 +22,7 @@ import { CreateOrEditContact } from './UseCase/CreateOrEditContact'
import { EditContact } from './UseCase/EditContact'
import { GetAllContacts } from './UseCase/GetAllContacts'
import { EncryptionProviderInterface } from '../Encryption/EncryptionProviderInterface'
import { Result } from '@standardnotes/domain-core'
export class ContactService
extends AbstractService<ContactServiceEvent>
@@ -34,6 +36,7 @@ export class ContactService
private user: UserClientInterface,
private selfContactManager: SelfContactManager,
private encryption: EncryptionProviderInterface,
private _deleteContact: DeleteContact,
private _findContact: FindContact,
private _getAllContacts: GetAllContacts,
private _createOrEditContact: CreateOrEditContact,
@@ -167,20 +170,15 @@ export class ContactService
return contact
}
async deleteContact(contact: TrustedContactInterface): Promise<void> {
if (contact.isMe) {
throw new Error('Cannot delete self')
}
await this.mutator.setItemToBeDeleted(contact)
await this.sync.sync()
async deleteContact(contact: TrustedContactInterface): Promise<Result<void>> {
return this._deleteContact.execute({ contact, ownUserUuid: this.session.userUuid })
}
getAllContacts(): TrustedContactInterface[] {
return this._getAllContacts.execute().getValue()
}
findTrustedContact(userUuid: string): TrustedContactInterface | undefined {
findContact(userUuid: string): TrustedContactInterface | undefined {
const result = this._findContact.execute({ userUuid })
if (result.isFailed()) {
return undefined
@@ -188,12 +186,12 @@ export class ContactService
return result.getValue()
}
findTrustedContactForServerUser(user: SharedVaultUserServerHash): TrustedContactInterface | undefined {
return this.findTrustedContact(user.user_uuid)
findContactForServerUser(user: SharedVaultUserServerHash): TrustedContactInterface | undefined {
return this.findContact(user.user_uuid)
}
findTrustedContactForInvite(invite: SharedVaultInviteServerHash): TrustedContactInterface | undefined {
return this.findTrustedContact(invite.sender_uuid)
findContactForInvite(invite: SharedVaultInviteServerHash): TrustedContactInterface | undefined {
return this.findContact(invite.sender_uuid)
}
getCollaborationIDForTrustedContact(contact: TrustedContactInterface): string {
@@ -205,7 +203,7 @@ export class ContactService
})
}
isItemAuthenticallySigned(item: DecryptedItemInterface): ItemSignatureValidationResult {
getItemSignatureStatus(item: DecryptedItemInterface): ItemSignatureValidationResult {
return this._validateItemSigner.execute(item)
}
@@ -2,6 +2,7 @@ import { DecryptedItemInterface, TrustedContactInterface } from '@standardnotes/
import { AbstractService } from '../Service/AbstractService'
import { SharedVaultInviteServerHash, SharedVaultUserServerHash } from '@standardnotes/responses'
import { ItemSignatureValidationResult } from './UseCase/Types/ItemSignatureValidationResult'
import { Result } from '@standardnotes/domain-core'
export enum ContactServiceEvent {}
@@ -26,13 +27,13 @@ export interface ContactServiceInterface extends AbstractService<ContactServiceE
contact: TrustedContactInterface,
params: { name: string; collaborationID: string },
): Promise<TrustedContactInterface>
deleteContact(contact: TrustedContactInterface): Promise<void>
deleteContact(contact: TrustedContactInterface): Promise<Result<void>>
getAllContacts(): TrustedContactInterface[]
getSelfContact(): TrustedContactInterface | undefined
findTrustedContact(userUuid: string): TrustedContactInterface | undefined
findTrustedContactForServerUser(user: SharedVaultUserServerHash): TrustedContactInterface | undefined
findTrustedContactForInvite(invite: SharedVaultInviteServerHash): TrustedContactInterface | undefined
findContact(userUuid: string): TrustedContactInterface | undefined
findContactForServerUser(user: SharedVaultUserServerHash): TrustedContactInterface | undefined
findContactForInvite(invite: SharedVaultInviteServerHash): TrustedContactInterface | undefined
isItemAuthenticallySigned(item: DecryptedItemInterface): ItemSignatureValidationResult
getItemSignatureStatus(item: DecryptedItemInterface): ItemSignatureValidationResult
}
@@ -0,0 +1,87 @@
import { ContactBelongsToVault } from './../../SharedVaults/UseCase/ContactBelongsToVault'
import { GetOwnedSharedVaults } from './../../SharedVaults/UseCase/GetOwnedSharedVaults'
import { SyncServiceInterface } from './../../Sync/SyncServiceInterface'
import { MutatorClientInterface } from './../../Mutator/MutatorClientInterface'
import { Result } from '@standardnotes/domain-core'
import { TrustedContactInterface } from '@standardnotes/models'
import { DeleteContact } from './DeleteContact'
describe('DeleteContact', () => {
let mutator: jest.Mocked<MutatorClientInterface>
let sync: jest.Mocked<SyncServiceInterface>
let getOwnedVaults: jest.Mocked<GetOwnedSharedVaults>
let contactBelongsToVault: jest.Mocked<ContactBelongsToVault>
let deleteContact: DeleteContact
let mockDto: {
contact: TrustedContactInterface
ownUserUuid: string
}
let mockVaults: { id: string }[]
beforeEach(() => {
mutator = {} as jest.Mocked<MutatorClientInterface>
sync = {} as jest.Mocked<SyncServiceInterface>
getOwnedVaults = {} as jest.Mocked<GetOwnedSharedVaults>
getOwnedVaults.execute = jest.fn()
contactBelongsToVault = {} as jest.Mocked<ContactBelongsToVault>
contactBelongsToVault.execute = jest.fn()
deleteContact = new DeleteContact(mutator, sync, getOwnedVaults, contactBelongsToVault)
mockDto = {
contact: { isMe: false } as TrustedContactInterface,
ownUserUuid: 'test-user-uuid',
}
mockVaults = [{ id: 'vault1' }, { id: 'vault2' }]
})
describe('execute', () => {
it('should throw error when deleting self', async () => {
mockDto.contact.isMe = true
await expect(deleteContact.execute(mockDto)).rejects.toThrow('Cannot delete self')
})
it('should return failure when getting owned vaults fails', async () => {
getOwnedVaults.execute = jest.fn().mockReturnValue(Result.fail('Failed to get owned vaults'))
const result = await deleteContact.execute(mockDto)
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Failed to get owned vaults')
})
it('should return failure when checking contact membership fails', async () => {
getOwnedVaults.execute = jest.fn().mockReturnValue(Result.ok(mockVaults))
contactBelongsToVault.execute = jest.fn().mockResolvedValue(Result.fail('Failed to check contact membership'))
const result = await deleteContact.execute(mockDto)
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Failed to check contact membership')
})
it('should return failure when contact belongs to an owned vault', async () => {
getOwnedVaults.execute = jest.fn().mockReturnValue(Result.ok(mockVaults))
contactBelongsToVault.execute = jest.fn().mockResolvedValue(Result.ok(true))
const result = await deleteContact.execute(mockDto)
expect(result.isFailed()).toBe(true)
expect(result.getError()).toBe('Cannot delete contact that belongs to an owned vault')
})
it('should return success when contact is successfully deleted', async () => {
getOwnedVaults.execute = jest.fn().mockReturnValue(Result.ok(mockVaults))
contactBelongsToVault.execute = jest.fn().mockResolvedValue(Result.ok(false))
mutator.setItemToBeDeleted = jest.fn().mockResolvedValue(undefined)
sync.sync = jest.fn().mockResolvedValue(undefined)
const result = await deleteContact.execute(mockDto)
expect(result.isFailed()).toBe(false)
})
})
})
@@ -0,0 +1,45 @@
import { ContactBelongsToVault } from './../../SharedVaults/UseCase/ContactBelongsToVault'
import { GetOwnedSharedVaults } from './../../SharedVaults/UseCase/GetOwnedSharedVaults'
import { SyncServiceInterface } from './../../Sync/SyncServiceInterface'
import { MutatorClientInterface } from './../../Mutator/MutatorClientInterface'
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
import { TrustedContactInterface } from '@standardnotes/models'
export class DeleteContact implements UseCaseInterface<void> {
constructor(
private mutator: MutatorClientInterface,
private sync: SyncServiceInterface,
private getOwnedVaults: GetOwnedSharedVaults,
private contactBelongsToVault: ContactBelongsToVault,
) {}
async execute(dto: { contact: TrustedContactInterface; ownUserUuid: string }): Promise<Result<void>> {
if (dto.contact.isMe) {
throw new Error('Cannot delete self')
}
const vaults = this.getOwnedVaults.execute({ userUuid: dto.ownUserUuid })
if (vaults.isFailed()) {
return Result.fail('Failed to get owned vaults')
}
for (const vault of vaults.getValue()) {
const contactBelongsToVault = await this.contactBelongsToVault.execute({
contact: dto.contact,
vault: vault,
})
if (contactBelongsToVault.isFailed()) {
return Result.fail('Failed to check contact membership')
}
if (contactBelongsToVault.getValue()) {
return Result.fail('Cannot delete contact that belongs to an owned vault')
}
}
await this.mutator.setItemToBeDeleted(dto.contact)
await this.sync.sync()
return Result.ok()
}
}
@@ -1,6 +1,6 @@
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
import { PkcKeyPair } from '@standardnotes/sncrypto-common'
import { ReuploadAllInvites } from '../../SharedVaults/UseCase/ReuploadAllInvites'
import { ReuploadAllInvites } from '../../VaultInvite/UseCase/ReuploadAllInvites'
import { ResendAllMessages } from '../../AsymmetricMessage/UseCase/ResendAllMessages'
export class HandleKeyPairChange implements UseCaseInterface<void> {
@@ -1,30 +1,21 @@
import { InternalEventBusInterface } from './../Internal/InternalEventBusInterface'
import { GetOwnedSharedVaults } from './UseCase/GetOwnedSharedVaults'
import { IsVaultOwner } from './../VaultUser/UseCase/IsVaultOwner'
import { EncryptionProviderInterface } from './../Encryption/EncryptionProviderInterface'
import { GetVaultUsers } from './UseCase/GetVaultUsers'
import { RemoveVaultMember } from './UseCase/RemoveSharedVaultMember'
import { DeleteSharedVault } from './UseCase/DeleteSharedVault'
import { ConvertToSharedVault } from './UseCase/ConvertToSharedVault'
import { ShareContactWithVault } from './UseCase/ShareContactWithVault'
import { DeleteThirdPartyVault } from './UseCase/DeleteExternalSharedVault'
import { LeaveVault } from './UseCase/LeaveSharedVault'
import { InviteToVault } from './UseCase/InviteToVault'
import { AcceptVaultInvite } from './UseCase/AcceptVaultInvite'
import { GetVaultContacts } from './UseCase/GetVaultContacts'
import { GetAllContacts } from './../Contacts/UseCase/GetAllContacts'
import { FindContact } from './../Contacts/UseCase/FindContact'
import { GetUntrustedPayload } from './../AsymmetricMessage/UseCase/GetUntrustedPayload'
import { GetTrustedPayload } from './../AsymmetricMessage/UseCase/GetTrustedPayload'
import { SendVaultDataChangedMessage } from './UseCase/SendVaultDataChangedMessage'
import { NotifyVaultUsersOfKeyRotation } from './UseCase/NotifyVaultUsersOfKeyRotation'
import { HandleKeyPairChange } from './../Contacts/UseCase/HandleKeyPairChange'
import { CreateSharedVault } from './UseCase/CreateSharedVault'
import { GetVault } from './../Vaults/UseCase/GetVault'
import { SharedVaultInvitesServer } from '@standardnotes/api'
import { SharedVaultService } from './SharedVaultService'
import { SyncServiceInterface } from '../Sync/SyncServiceInterface'
import { ItemManagerInterface } from '../Item/ItemManagerInterface'
import { SessionsClientInterface } from '../Session/SessionsClientInterface'
import { VaultServiceInterface } from '../Vaults/VaultServiceInterface'
import { InternalEventBusInterface } from '../..'
import { ContactPublicKeySetInterface, TrustedContactInterface } from '@standardnotes/models'
describe('SharedVaultService', () => {
@@ -39,57 +30,38 @@ describe('SharedVaultService', () => {
const encryption = {} as jest.Mocked<EncryptionProviderInterface>
const session = {} as jest.Mocked<SessionsClientInterface>
const vaults = {} as jest.Mocked<VaultServiceInterface>
const invitesServer = {} as jest.Mocked<SharedVaultInvitesServer>
const getVault = {} as jest.Mocked<GetVault>
const getOwnedVaults = {} as jest.Mocked<GetOwnedSharedVaults>
const createSharedVaultUseCase = {} as jest.Mocked<CreateSharedVault>
const handleKeyPairChange = {} as jest.Mocked<HandleKeyPairChange>
const notifyVaultUsersOfKeyRotation = {} as jest.Mocked<NotifyVaultUsersOfKeyRotation>
const sendVaultDataChangeMessage = {} as jest.Mocked<SendVaultDataChangedMessage>
const getTrustedPayload = {} as jest.Mocked<GetTrustedPayload>
const getUntrustedPayload = {} as jest.Mocked<GetUntrustedPayload>
const findContact = {} as jest.Mocked<FindContact>
const getAllContacts = {} as jest.Mocked<GetAllContacts>
const getVaultContacts = {} as jest.Mocked<GetVaultContacts>
const acceptVaultInvite = {} as jest.Mocked<AcceptVaultInvite>
const inviteToVault = {} as jest.Mocked<InviteToVault>
const leaveVault = {} as jest.Mocked<LeaveVault>
const deleteThirdPartyVault = {} as jest.Mocked<DeleteThirdPartyVault>
const shareContactWithVault = {} as jest.Mocked<ShareContactWithVault>
const convertToSharedVault = {} as jest.Mocked<ConvertToSharedVault>
const deleteSharedVaultUseCase = {} as jest.Mocked<DeleteSharedVault>
const removeVaultMember = {} as jest.Mocked<RemoveVaultMember>
const getSharedVaultUsersUseCase = {} as jest.Mocked<GetVaultUsers>
const isVaultAdmin = {} as jest.Mocked<IsVaultOwner>
const eventBus = {} as jest.Mocked<InternalEventBusInterface>
eventBus.addEventHandler = jest.fn()
service = new SharedVaultService(
sync,
items,
encryption,
session,
vaults,
invitesServer,
getVault,
getOwnedVaults,
createSharedVaultUseCase,
handleKeyPairChange,
notifyVaultUsersOfKeyRotation,
sendVaultDataChangeMessage,
getTrustedPayload,
getUntrustedPayload,
findContact,
getAllContacts,
getVaultContacts,
acceptVaultInvite,
inviteToVault,
leaveVault,
deleteThirdPartyVault,
shareContactWithVault,
convertToSharedVault,
deleteSharedVaultUseCase,
removeVaultMember,
getSharedVaultUsersUseCase,
isVaultAdmin,
eventBus,
)
})
@@ -1,114 +1,70 @@
import { UserKeyPairChangedEventData } from './../Session/UserKeyPairChangedEventData'
import { InviteToVault } from './UseCase/InviteToVault'
import {
ClientDisplayableError,
SharedVaultInviteServerHash,
isErrorResponse,
SharedVaultUserServerHash,
isClientDisplayableError,
SharedVaultPermission,
UserEventType,
} from '@standardnotes/responses'
import { SharedVaultInvitesServer } from '@standardnotes/api'
import { ClientDisplayableError, UserEventType } from '@standardnotes/responses'
import {
DecryptedItemInterface,
PayloadEmitSource,
TrustedContactInterface,
SharedVaultListingInterface,
VaultListingInterface,
AsymmetricMessageSharedVaultInvite,
KeySystemRootKeyStorageMode,
} from '@standardnotes/models'
import { SharedVaultServiceInterface } from './SharedVaultServiceInterface'
import { SharedVaultServiceEvent, SharedVaultServiceEventPayload } from './SharedVaultServiceEvent'
import { GetVaultUsers } from './UseCase/GetVaultUsers'
import { RemoveVaultMember } from './UseCase/RemoveSharedVaultMember'
import { AbstractService } from '../Service/AbstractService'
import { InternalEventHandlerInterface } from '../Internal/InternalEventHandlerInterface'
import { SyncServiceInterface } from '../Sync/SyncServiceInterface'
import { ItemManagerInterface } from '../Item/ItemManagerInterface'
import { SessionsClientInterface } from '../Session/SessionsClientInterface'
import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
import { SyncEvent, SyncEventReceivedSharedVaultInvitesData } from '../Event/SyncEvent'
import { SyncEvent } from '../Event/SyncEvent'
import { SessionEvent } from '../Session/SessionEvent'
import { InternalEventInterface } from '../Internal/InternalEventInterface'
import { LeaveVault } from './UseCase/LeaveSharedVault'
import { VaultServiceInterface } from '../Vaults/VaultServiceInterface'
import { UserEventServiceEvent, UserEventServiceEventPayload } from '../UserEvent/UserEventServiceEvent'
import { DeleteThirdPartyVault } from './UseCase/DeleteExternalSharedVault'
import { DeleteSharedVault } from './UseCase/DeleteSharedVault'
import { VaultServiceEvent, VaultServiceEventPayload } from '../Vaults/VaultServiceEvent'
import { AcceptVaultInvite } from './UseCase/AcceptVaultInvite'
import { GetTrustedPayload } from '../AsymmetricMessage/UseCase/GetTrustedPayload'
import { PendingSharedVaultInviteRecord } from './PendingSharedVaultInviteRecord'
import { GetUntrustedPayload } from '../AsymmetricMessage/UseCase/GetUntrustedPayload'
import { ShareContactWithVault } from './UseCase/ShareContactWithVault'
import { GetVaultContacts } from './UseCase/GetVaultContacts'
import { NotifyVaultUsersOfKeyRotation } from './UseCase/NotifyVaultUsersOfKeyRotation'
import { CreateSharedVault } from './UseCase/CreateSharedVault'
import { SendVaultDataChangedMessage } from './UseCase/SendVaultDataChangedMessage'
import { ConvertToSharedVault } from './UseCase/ConvertToSharedVault'
import { GetVault } from '../Vaults/UseCase/GetVault'
import { ContentType, Result } from '@standardnotes/domain-core'
import { ContentType } from '@standardnotes/domain-core'
import { HandleKeyPairChange } from '../Contacts/UseCase/HandleKeyPairChange'
import { FindContact } from '../Contacts/UseCase/FindContact'
import { GetAllContacts } from '../Contacts/UseCase/GetAllContacts'
import { EncryptionProviderInterface } from '../Encryption/EncryptionProviderInterface'
import { IsVaultOwner } from '../VaultUser/UseCase/IsVaultOwner'
import { GetOwnedSharedVaults } from './UseCase/GetOwnedSharedVaults'
export class SharedVaultService
extends AbstractService<SharedVaultServiceEvent, SharedVaultServiceEventPayload>
implements SharedVaultServiceInterface, InternalEventHandlerInterface
{
private pendingInvites: Record<string, PendingSharedVaultInviteRecord> = {}
constructor(
private sync: SyncServiceInterface,
private items: ItemManagerInterface,
private encryption: EncryptionProviderInterface,
private session: SessionsClientInterface,
private vaults: VaultServiceInterface,
private invitesServer: SharedVaultInvitesServer,
private getVault: GetVault,
private createSharedVaultUseCase: CreateSharedVault,
private handleKeyPairChange: HandleKeyPairChange,
private notifyVaultUsersOfKeyRotation: NotifyVaultUsersOfKeyRotation,
private sendVaultDataChangeMessage: SendVaultDataChangedMessage,
private getTrustedPayload: GetTrustedPayload,
private getUntrustedPayload: GetUntrustedPayload,
private findContact: FindContact,
private getAllContacts: GetAllContacts,
private getVaultContacts: GetVaultContacts,
private acceptVaultInvite: AcceptVaultInvite,
private inviteToVault: InviteToVault,
private leaveVault: LeaveVault,
private deleteThirdPartyVault: DeleteThirdPartyVault,
private shareContactWithVault: ShareContactWithVault,
private convertToSharedVault: ConvertToSharedVault,
private deleteSharedVaultUseCase: DeleteSharedVault,
private removeVaultMember: RemoveVaultMember,
private getSharedVaultUsersUseCase: GetVaultUsers,
private _getVault: GetVault,
private _getOwnedSharedVaults: GetOwnedSharedVaults,
private _createSharedVault: CreateSharedVault,
private _handleKeyPairChange: HandleKeyPairChange,
private _notifyVaultUsersOfKeyRotation: NotifyVaultUsersOfKeyRotation,
private _sendVaultDataChangeMessage: SendVaultDataChangedMessage,
private _findContact: FindContact,
private _deleteThirdPartyVault: DeleteThirdPartyVault,
private _shareContactWithVault: ShareContactWithVault,
private _convertToSharedVault: ConvertToSharedVault,
private _deleteSharedVault: DeleteSharedVault,
private _isVaultAdmin: IsVaultOwner,
eventBus: InternalEventBusInterface,
) {
super(eventBus)
eventBus.addEventHandler(this, SessionEvent.UserKeyPairChanged)
eventBus.addEventHandler(this, UserEventServiceEvent.UserEventReceived)
eventBus.addEventHandler(this, VaultServiceEvent.VaultRootKeyRotated)
this.eventDisposers.push(
items.addObserver<TrustedContactInterface>(
ContentType.TYPES.TrustedContact,
async ({ changed, inserted, source }) => {
await this.reprocessCachedInvitesTrustStatusAfterTrustedContactsChange()
if (source === PayloadEmitSource.LocalChanged && inserted.length > 0) {
void this.handleCreationOfNewTrustedContacts(inserted)
}
if (source === PayloadEmitSource.LocalChanged && changed.length > 0) {
void this.handleTrustedContactsChange(changed)
}
},
),
items.addObserver<TrustedContactInterface>(ContentType.TYPES.TrustedContact, async ({ changed, source }) => {
if (source === PayloadEmitSource.LocalChanged && changed.length > 0) {
void this.handleTrustedContactsChange(changed)
}
}),
)
this.eventDisposers.push(
@@ -120,54 +76,87 @@ export class SharedVaultService
)
}
override deinit(): void {
super.deinit()
;(this.items as unknown) = undefined
;(this.encryption as unknown) = undefined
;(this.session as unknown) = undefined
;(this._getVault as unknown) = undefined
;(this._createSharedVault as unknown) = undefined
;(this._handleKeyPairChange as unknown) = undefined
;(this._notifyVaultUsersOfKeyRotation as unknown) = undefined
;(this._sendVaultDataChangeMessage as unknown) = undefined
;(this._findContact as unknown) = undefined
;(this._deleteThirdPartyVault as unknown) = undefined
;(this._shareContactWithVault as unknown) = undefined
;(this._convertToSharedVault as unknown) = undefined
;(this._deleteSharedVault as unknown) = undefined
;(this._isVaultAdmin as unknown) = undefined
}
async handleEvent(event: InternalEventInterface): Promise<void> {
if (event.type === SessionEvent.UserKeyPairChanged) {
void this.invitesServer.deleteAllInboundInvites()
const eventData = event.payload as UserKeyPairChangedEventData
void this.handleKeyPairChange.execute({
newKeys: eventData.current,
previousKeys: eventData.previous,
})
} else if (event.type === UserEventServiceEvent.UserEventReceived) {
await this.handleUserEvent(event.payload as UserEventServiceEventPayload)
} else if (event.type === VaultServiceEvent.VaultRootKeyRotated) {
const payload = event.payload as VaultServiceEventPayload[VaultServiceEvent.VaultRootKeyRotated]
await this.handleVaultRootKeyRotatedEvent(payload.vault)
} else if (event.type === SyncEvent.ReceivedSharedVaultInvites) {
await this.processInboundInvites(event.payload as SyncEventReceivedSharedVaultInvitesData)
} else if (event.type === SyncEvent.ReceivedRemoteSharedVaults) {
void this.notifyCollaborationStatusChanged()
switch (event.type) {
case SessionEvent.UserKeyPairChanged: {
const eventData = event.payload as UserKeyPairChangedEventData
void this._handleKeyPairChange.execute({
newKeys: eventData.current,
previousKeys: eventData.previous,
})
break
}
case UserEventServiceEvent.UserEventReceived:
await this.handleUserEvent(event.payload as UserEventServiceEventPayload)
break
case VaultServiceEvent.VaultRootKeyRotated: {
const payload = event.payload as VaultServiceEventPayload[VaultServiceEvent.VaultRootKeyRotated]
await this.handleVaultRootKeyRotatedEvent(payload.vault)
break
}
case SyncEvent.ReceivedRemoteSharedVaults:
void this.notifyEventSync(SharedVaultServiceEvent.SharedVaultStatusChanged)
break
}
}
private async handleUserEvent(event: UserEventServiceEventPayload): Promise<void> {
if (event.eventPayload.eventType === UserEventType.RemovedFromSharedVault) {
const vault = this.getVault.execute<SharedVaultListingInterface>({
sharedVaultUuid: event.eventPayload.sharedVaultUuid,
})
if (!vault.isFailed()) {
await this.deleteThirdPartyVault.execute(vault.getValue())
switch (event.eventPayload.eventType) {
case UserEventType.RemovedFromSharedVault: {
const vault = this._getVault.execute<SharedVaultListingInterface>({
sharedVaultUuid: event.eventPayload.sharedVaultUuid,
})
if (!vault.isFailed()) {
await this._deleteThirdPartyVault.execute(vault.getValue())
}
break
}
} else if (event.eventPayload.eventType === UserEventType.SharedVaultItemRemoved) {
const item = this.items.findItem(event.eventPayload.itemUuid)
if (item) {
this.items.removeItemsLocally([item])
case UserEventType.SharedVaultItemRemoved: {
const item = this.items.findItem(event.eventPayload.itemUuid)
if (item) {
this.items.removeItemsLocally([item])
}
break
}
}
}
private isCurrentUserVaultOwner(sharedVault: SharedVaultListingInterface): boolean {
if (!sharedVault.sharing.ownerUserUuid) {
throw new Error(`Shared vault ${sharedVault.sharing.sharedVaultUuid} does not have an owner user uuid`)
}
return sharedVault.sharing.ownerUserUuid === this.session.userUuid
}
private async handleVaultRootKeyRotatedEvent(vault: VaultListingInterface): Promise<void> {
if (!vault.isSharedVaultListing()) {
return
}
if (!this.isCurrentUserSharedVaultOwner(vault)) {
if (!this.isCurrentUserVaultOwner(vault)) {
return
}
await this.notifyVaultUsersOfKeyRotation.execute({
await this._notifyVaultUsersOfKeyRotation.execute({
sharedVault: vault,
senderUuid: this.session.getSureUser().uuid,
keys: {
@@ -183,7 +172,7 @@ export class SharedVaultService
userInputtedPassword: string | undefined
storagePreference?: KeySystemRootKeyStorageMode
}): Promise<VaultListingInterface | ClientDisplayableError> {
return this.createSharedVaultUseCase.execute({
return this._createSharedVault.execute({
vaultName: dto.name,
vaultDescription: dto.description,
userInputtedPassword: dto.userInputtedPassword,
@@ -194,48 +183,7 @@ export class SharedVaultService
async convertVaultToSharedVault(
vault: VaultListingInterface,
): Promise<SharedVaultListingInterface | ClientDisplayableError> {
return this.convertToSharedVault.execute({ vault })
}
public getCachedPendingInviteRecords(): PendingSharedVaultInviteRecord[] {
return Object.values(this.pendingInvites)
}
private getAllSharedVaults(): SharedVaultListingInterface[] {
const vaults = this.vaults.getVaults().filter((vault) => vault.isSharedVaultListing())
return vaults as SharedVaultListingInterface[]
}
private findSharedVault(sharedVaultUuid: string): SharedVaultListingInterface | undefined {
const result = this.getVault.execute<SharedVaultListingInterface>({ sharedVaultUuid })
if (result.isFailed()) {
return undefined
}
return result.getValue()
}
public isCurrentUserSharedVaultAdmin(sharedVault: SharedVaultListingInterface): boolean {
if (!sharedVault.sharing.ownerUserUuid) {
throw new Error(`Shared vault ${sharedVault.sharing.sharedVaultUuid} does not have an owner user uuid`)
}
return sharedVault.sharing.ownerUserUuid === this.session.userUuid
}
public isCurrentUserSharedVaultOwner(sharedVault: SharedVaultListingInterface): boolean {
if (!sharedVault.sharing.ownerUserUuid) {
throw new Error(`Shared vault ${sharedVault.sharing.sharedVaultUuid} does not have an owner user uuid`)
}
return sharedVault.sharing.ownerUserUuid === this.session.userUuid
}
public isSharedVaultUserSharedVaultOwner(user: SharedVaultUserServerHash): boolean {
const vault = this.findSharedVault(user.shared_vault_uuid)
return vault != undefined && vault.sharing.ownerUserUuid === user.user_uuid
}
private async handleCreationOfNewTrustedContacts(_contacts: TrustedContactInterface[]): Promise<void> {
await this.downloadInboundInvites()
return this._convertToSharedVault.execute({ vault })
}
private async handleTrustedContactsChange(contacts: TrustedContactInterface[]): Promise<void> {
@@ -254,7 +202,7 @@ export class SharedVaultService
continue
}
await this.sendVaultDataChangeMessage.execute({
await this._sendVaultDataChangeMessage.execute({
vault,
senderUuid: this.session.getSureUser().uuid,
keys: {
@@ -265,220 +213,8 @@ export class SharedVaultService
}
}
public async downloadInboundInvites(): Promise<ClientDisplayableError | SharedVaultInviteServerHash[]> {
const response = await this.invitesServer.getInboundUserInvites()
if (isErrorResponse(response)) {
return ClientDisplayableError.FromString(`Failed to get inbound user invites ${response}`)
}
this.pendingInvites = {}
await this.processInboundInvites(response.data.invites)
return response.data.invites
}
public async getOutboundInvites(
sharedVault?: SharedVaultListingInterface,
): Promise<SharedVaultInviteServerHash[] | ClientDisplayableError> {
const response = await this.invitesServer.getOutboundUserInvites()
if (isErrorResponse(response)) {
return ClientDisplayableError.FromString(`Failed to get outbound user invites ${response}`)
}
if (sharedVault) {
return response.data.invites.filter((invite) => invite.shared_vault_uuid === sharedVault.sharing.sharedVaultUuid)
}
return response.data.invites
}
public async deleteInvite(invite: SharedVaultInviteServerHash): Promise<ClientDisplayableError | void> {
const response = await this.invitesServer.deleteInvite({
sharedVaultUuid: invite.shared_vault_uuid,
inviteUuid: invite.uuid,
})
if (isErrorResponse(response)) {
return ClientDisplayableError.FromString(`Failed to delete invite ${response}`)
}
delete this.pendingInvites[invite.uuid]
}
public async deleteSharedVault(sharedVault: SharedVaultListingInterface): Promise<ClientDisplayableError | void> {
return this.deleteSharedVaultUseCase.execute({ sharedVault })
}
private async reprocessCachedInvitesTrustStatusAfterTrustedContactsChange(): Promise<void> {
const cachedInvites = this.getCachedPendingInviteRecords().map((record) => record.invite)
await this.processInboundInvites(cachedInvites)
}
private async processInboundInvites(invites: SharedVaultInviteServerHash[]): Promise<void> {
if (invites.length === 0) {
return
}
for (const invite of invites) {
const sender = this.findContact.execute({ userUuid: invite.sender_uuid })
if (!sender.isFailed()) {
const trustedMessage = this.getTrustedPayload.execute<AsymmetricMessageSharedVaultInvite>({
message: invite,
privateKey: this.encryption.getKeyPair().privateKey,
sender: sender.getValue(),
})
if (!trustedMessage.isFailed()) {
this.pendingInvites[invite.uuid] = {
invite,
message: trustedMessage.getValue(),
trusted: true,
}
continue
}
}
const untrustedMessage = this.getUntrustedPayload.execute<AsymmetricMessageSharedVaultInvite>({
message: invite,
privateKey: this.encryption.getKeyPair().privateKey,
})
if (!untrustedMessage.isFailed()) {
this.pendingInvites[invite.uuid] = {
invite,
message: untrustedMessage.getValue(),
trusted: false,
}
}
}
await this.notifyCollaborationStatusChanged()
}
private async notifyCollaborationStatusChanged(): Promise<void> {
await this.notifyEventSync(SharedVaultServiceEvent.SharedVaultStatusChanged)
}
async acceptPendingSharedVaultInvite(pendingInvite: PendingSharedVaultInviteRecord): Promise<void> {
if (!pendingInvite.trusted) {
throw new Error('Cannot accept untrusted invite')
}
await this.acceptVaultInvite.execute({ invite: pendingInvite.invite, message: pendingInvite.message })
delete this.pendingInvites[pendingInvite.invite.uuid]
void this.sync.sync()
await this.decryptErroredItemsAfterInviteAccept()
await this.sync.syncSharedVaultsFromScratch([pendingInvite.invite.shared_vault_uuid])
}
private async decryptErroredItemsAfterInviteAccept(): Promise<void> {
await this.encryption.decryptErroredPayloads()
}
public async getInvitableContactsForSharedVault(
sharedVault: SharedVaultListingInterface,
): Promise<TrustedContactInterface[]> {
const users = await this.getSharedVaultUsers(sharedVault)
if (!users) {
return []
}
const contacts = this.getAllContacts.execute()
if (contacts.isFailed()) {
return []
}
return contacts.getValue().filter((contact) => {
const isContactAlreadyInVault = users.some((user) => user.user_uuid === contact.contactUuid)
return !isContactAlreadyInVault
})
}
private async getSharedVaultContacts(sharedVault: SharedVaultListingInterface): Promise<TrustedContactInterface[]> {
const contacts = await this.getVaultContacts.execute(sharedVault.sharing.sharedVaultUuid)
if (contacts.isFailed()) {
return []
}
return contacts.getValue()
}
async inviteContactToSharedVault(
sharedVault: SharedVaultListingInterface,
contact: TrustedContactInterface,
permissions: SharedVaultPermission,
): Promise<Result<SharedVaultInviteServerHash>> {
const sharedVaultContacts = await this.getSharedVaultContacts(sharedVault)
const result = await this.inviteToVault.execute({
keys: {
encryption: this.encryption.getKeyPair(),
signing: this.encryption.getSigningKeyPair(),
},
senderUuid: this.session.getSureUser().uuid,
sharedVault,
recipient: contact,
sharedVaultContacts,
permissions,
})
void this.notifyCollaborationStatusChanged()
await this.sync.sync()
return result
}
async removeUserFromSharedVault(
sharedVault: SharedVaultListingInterface,
userUuid: string,
): Promise<ClientDisplayableError | void> {
if (!this.isCurrentUserSharedVaultAdmin(sharedVault)) {
throw new Error('Only vault admins can remove users')
}
if (this.vaults.isVaultLocked(sharedVault)) {
throw new Error('Cannot remove user from locked vault')
}
const result = await this.removeVaultMember.execute({
sharedVaultUuid: sharedVault.sharing.sharedVaultUuid,
userUuid,
})
if (isClientDisplayableError(result)) {
return result
}
void this.notifyCollaborationStatusChanged()
await this.vaults.rotateVaultRootKey(sharedVault)
}
async leaveSharedVault(sharedVault: SharedVaultListingInterface): Promise<ClientDisplayableError | void> {
const result = await this.leaveVault.execute({
sharedVault: sharedVault,
userUuid: this.session.getSureUser().uuid,
})
if (isClientDisplayableError(result)) {
return result
}
void this.notifyCollaborationStatusChanged()
}
async getSharedVaultUsers(
sharedVault: SharedVaultListingInterface,
): Promise<SharedVaultUserServerHash[] | undefined> {
return this.getSharedVaultUsersUseCase.execute({ sharedVaultUuid: sharedVault.sharing.sharedVaultUuid })
return this._deleteSharedVault.execute({ sharedVault })
}
async shareContactWithVaults(contact: TrustedContactInterface): Promise<void> {
@@ -486,10 +222,10 @@ export class SharedVaultService
throw new Error('Cannot share self contact')
}
const ownedVaults = this.getAllSharedVaults().filter(this.isCurrentUserSharedVaultAdmin.bind(this))
const ownedVaults = this._getOwnedSharedVaults.execute({ userUuid: this.session.userUuid }).getValue()
for (const vault of ownedVaults) {
await this.shareContactWithVault.execute({
await this._shareContactWithVault.execute({
keys: {
encryption: this.encryption.getKeyPair(),
signing: this.encryption.getSigningKeyPair(),
@@ -506,7 +242,7 @@ export class SharedVaultService
return undefined
}
const contact = this.findContact.execute({ userUuid: item.last_edited_by_uuid })
const contact = this._findContact.execute({ userUuid: item.last_edited_by_uuid })
return contact.isFailed() ? undefined : contact.getValue()
}
@@ -516,37 +252,8 @@ export class SharedVaultService
return undefined
}
const contact = this.findContact.execute({ userUuid: item.user_uuid })
const contact = this._findContact.execute({ userUuid: item.user_uuid })
return contact.isFailed() ? undefined : contact.getValue()
}
override deinit(): void {
super.deinit()
;(this.sync as unknown) = undefined
;(this.items as unknown) = undefined
;(this.encryption as unknown) = undefined
;(this.session as unknown) = undefined
;(this.vaults as unknown) = undefined
;(this.invitesServer as unknown) = undefined
;(this.getVault as unknown) = undefined
;(this.createSharedVaultUseCase as unknown) = undefined
;(this.handleKeyPairChange as unknown) = undefined
;(this.notifyVaultUsersOfKeyRotation as unknown) = undefined
;(this.sendVaultDataChangeMessage as unknown) = undefined
;(this.getTrustedPayload as unknown) = undefined
;(this.getUntrustedPayload as unknown) = undefined
;(this.findContact as unknown) = undefined
;(this.getAllContacts as unknown) = undefined
;(this.getVaultContacts as unknown) = undefined
;(this.acceptVaultInvite as unknown) = undefined
;(this.inviteToVault as unknown) = undefined
;(this.leaveVault as unknown) = undefined
;(this.deleteThirdPartyVault as unknown) = undefined
;(this.shareContactWithVault as unknown) = undefined
;(this.convertToSharedVault as unknown) = undefined
;(this.deleteSharedVaultUseCase as unknown) = undefined
;(this.removeVaultMember as unknown) = undefined
;(this.getSharedVaultUsersUseCase as unknown) = undefined
}
}
@@ -1,9 +1,4 @@
import {
ClientDisplayableError,
SharedVaultInviteServerHash,
SharedVaultUserServerHash,
SharedVaultPermission,
} from '@standardnotes/responses'
import { ClientDisplayableError } from '@standardnotes/responses'
import {
DecryptedItemInterface,
TrustedContactInterface,
@@ -13,8 +8,6 @@ import {
} from '@standardnotes/models'
import { AbstractService } from '../Service/AbstractService'
import { SharedVaultServiceEvent, SharedVaultServiceEventPayload } from './SharedVaultServiceEvent'
import { PendingSharedVaultInviteRecord } from './PendingSharedVaultInviteRecord'
import { Result } from '@standardnotes/domain-core'
export interface SharedVaultServiceInterface
extends AbstractService<SharedVaultServiceEvent, SharedVaultServiceEventPayload> {
@@ -25,32 +18,8 @@ export interface SharedVaultServiceInterface
storagePreference?: KeySystemRootKeyStorageMode
}): Promise<VaultListingInterface | ClientDisplayableError>
deleteSharedVault(sharedVault: SharedVaultListingInterface): Promise<ClientDisplayableError | void>
convertVaultToSharedVault(vault: VaultListingInterface): Promise<SharedVaultListingInterface | ClientDisplayableError>
inviteContactToSharedVault(
sharedVault: SharedVaultListingInterface,
contact: TrustedContactInterface,
permissions: SharedVaultPermission,
): Promise<Result<SharedVaultInviteServerHash>>
removeUserFromSharedVault(
sharedVault: SharedVaultListingInterface,
userUuid: string,
): Promise<ClientDisplayableError | void>
leaveSharedVault(sharedVault: SharedVaultListingInterface): Promise<ClientDisplayableError | void>
getSharedVaultUsers(sharedVault: SharedVaultListingInterface): Promise<SharedVaultUserServerHash[] | undefined>
isSharedVaultUserSharedVaultOwner(user: SharedVaultUserServerHash): boolean
isCurrentUserSharedVaultAdmin(sharedVault: SharedVaultListingInterface): boolean
getItemLastEditedBy(item: DecryptedItemInterface): TrustedContactInterface | undefined
getItemSharedBy(item: DecryptedItemInterface): TrustedContactInterface | undefined
downloadInboundInvites(): Promise<ClientDisplayableError | SharedVaultInviteServerHash[]>
getOutboundInvites(
sharedVault?: SharedVaultListingInterface,
): Promise<SharedVaultInviteServerHash[] | ClientDisplayableError>
acceptPendingSharedVaultInvite(pendingInvite: PendingSharedVaultInviteRecord): Promise<void>
getCachedPendingInviteRecords(): PendingSharedVaultInviteRecord[]
getInvitableContactsForSharedVault(sharedVault: SharedVaultListingInterface): Promise<TrustedContactInterface[]>
deleteInvite(invite: SharedVaultInviteServerHash): Promise<ClientDisplayableError | void>
}
@@ -0,0 +1,23 @@
import { GetVaultUsers } from './../../VaultUser/UseCase/GetVaultUsers'
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
import { SharedVaultListingInterface, TrustedContactInterface } from '@standardnotes/models'
export class ContactBelongsToVault implements UseCaseInterface<boolean> {
constructor(private getVaultUsers: GetVaultUsers) {}
async execute(dto: {
contact: TrustedContactInterface
vault: SharedVaultListingInterface
}): Promise<Result<boolean>> {
const users = await this.getVaultUsers.execute({
sharedVaultUuid: dto.vault.sharing.sharedVaultUuid,
readFromCache: false,
})
if (users.isFailed()) {
return Result.fail('Failed to get vault users')
}
return Result.ok(users.getValue().some((u) => u.user_uuid === dto.contact.contactUuid))
}
}
@@ -0,0 +1,23 @@
import { IsVaultOwner } from './../../VaultUser/UseCase/IsVaultOwner'
import { Result, SyncUseCaseInterface } from '@standardnotes/domain-core'
import { SharedVaultListingInterface } from '@standardnotes/models'
import { GetSharedVaults } from './GetSharedVaults'
export class GetOwnedSharedVaults implements SyncUseCaseInterface<SharedVaultListingInterface[]> {
constructor(private getSharedVaults: GetSharedVaults, private isVaultOwnwer: IsVaultOwner) {}
execute(dto: { userUuid: string }): Result<SharedVaultListingInterface[]> {
const sharedVaults = this.getSharedVaults.execute().getValue()
const ownedVaults = sharedVaults.filter((vault) => {
return this.isVaultOwnwer
.execute({
sharedVault: vault,
userUuid: dto.userUuid,
})
.getValue()
})
return Result.ok(ownedVaults)
}
}
@@ -0,0 +1,16 @@
import { GetVaults } from './../../Vaults/UseCase/GetVaults'
import { Result, SyncUseCaseInterface } from '@standardnotes/domain-core'
import { SharedVaultListingInterface } from '@standardnotes/models'
export class GetSharedVaults implements SyncUseCaseInterface<SharedVaultListingInterface[]> {
constructor(private getVaults: GetVaults) {}
execute(): Result<SharedVaultListingInterface[]> {
const vaults = this.getVaults
.execute()
.getValue()
.filter((vault) => vault.isSharedVaultListing())
return Result.ok(vaults as SharedVaultListingInterface[])
}
}
@@ -1,16 +0,0 @@
import { SharedVaultUserServerHash, isErrorResponse } from '@standardnotes/responses'
import { SharedVaultUsersServerInterface } from '@standardnotes/api'
export class GetVaultUsers {
constructor(private vaultUsersServer: SharedVaultUsersServerInterface) {}
async execute(params: { sharedVaultUuid: string }): Promise<SharedVaultUserServerHash[] | undefined> {
const response = await this.vaultUsersServer.getSharedVaultUsers({ sharedVaultUuid: params.sharedVaultUuid })
if (isErrorResponse(response)) {
return undefined
}
return response.data.users
}
}
@@ -4,8 +4,8 @@ import { SharedVaultInviteServerHash, isErrorResponse } from '@standardnotes/res
import { SendVaultKeyChangedMessage } from './SendVaultKeyChangedMessage'
import { PkcKeyPair } from '@standardnotes/sncrypto-common'
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
import { InviteToVault } from './InviteToVault'
import { GetVaultContacts } from './GetVaultContacts'
import { InviteToVault } from '../../VaultInvite/UseCase/InviteToVault'
import { GetVaultContacts } from '../../VaultUser/UseCase/GetVaultContacts'
import { DecryptOwnMessage } from '../../Encryption/UseCase/Asymmetric/DecryptOwnMessage'
import { FindContact } from '../../Contacts/UseCase/FindContact'
@@ -44,7 +44,10 @@ export class NotifyVaultUsersOfKeyRotation implements UseCaseInterface<void> {
await this.deleteAllInvites(params.sharedVault.sharing.sharedVaultUuid)
const contacts = await this.getVaultContacts.execute(params.sharedVault.sharing.sharedVaultUuid)
const contacts = await this.getVaultContacts.execute({
sharedVaultUuid: params.sharedVault.sharing.sharedVaultUuid,
readFromCache: false,
})
for (const invite of existingInvites.getValue()) {
const recipient = this.findContact.execute({ userUuid: invite.user_uuid })
@@ -5,7 +5,7 @@ import {
TrustedContactInterface,
} from '@standardnotes/models'
import { AsymmetricMessageServerHash } from '@standardnotes/responses'
import { GetVaultUsers } from './GetVaultUsers'
import { GetVaultUsers } from '../../VaultUser/UseCase/GetVaultUsers'
import { PkcKeyPair } from '@standardnotes/sncrypto-common'
import { SendMessage } from '../../AsymmetricMessage/UseCase/SendMessage'
import { EncryptMessage } from '../../Encryption/UseCase/Asymmetric/EncryptMessage'
@@ -29,13 +29,16 @@ export class SendVaultDataChangedMessage implements UseCaseInterface<void> {
signing: PkcKeyPair
}
}): Promise<Result<void>> {
const users = await this.getVaultUsers.execute({ sharedVaultUuid: params.vault.sharing.sharedVaultUuid })
if (!users) {
const users = await this.getVaultUsers.execute({
sharedVaultUuid: params.vault.sharing.sharedVaultUuid,
readFromCache: false,
})
if (users.isFailed()) {
return Result.fail('Cannot send metadata changed message; users not found')
}
const errors: string[] = []
for (const user of users) {
for (const user of users.getValue()) {
if (user.user_uuid === params.senderUuid) {
continue
}
@@ -5,7 +5,7 @@ import {
TrustedContactInterface,
} from '@standardnotes/models'
import { AsymmetricMessageServerHash } from '@standardnotes/responses'
import { GetVaultUsers } from './GetVaultUsers'
import { GetVaultUsers } from '../../VaultUser/UseCase/GetVaultUsers'
import { PkcKeyPair } from '@standardnotes/sncrypto-common'
import { SendMessage } from '../../AsymmetricMessage/UseCase/SendMessage'
import { EncryptMessage } from '../../Encryption/UseCase/Asymmetric/EncryptMessage'
@@ -32,14 +32,14 @@ export class SendVaultKeyChangedMessage implements UseCaseInterface<void> {
signing: PkcKeyPair
}
}): Promise<Result<void>> {
const users = await this.getVaultUsers.execute({ sharedVaultUuid: params.sharedVaultUuid })
if (!users) {
const users = await this.getVaultUsers.execute({ sharedVaultUuid: params.sharedVaultUuid, readFromCache: false })
if (users.isFailed()) {
return Result.fail('Cannot send root key changed message; users not found')
}
const errors: string[] = []
for (const user of users) {
for (const user of users.getValue()) {
if (user.user_uuid === params.senderUuid) {
continue
}
@@ -8,7 +8,7 @@ import { SendMessage } from '../../AsymmetricMessage/UseCase/SendMessage'
import { EncryptMessage } from '../../Encryption/UseCase/Asymmetric/EncryptMessage'
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
import { FindContact } from '../../Contacts/UseCase/FindContact'
import { GetVaultUsers } from './GetVaultUsers'
import { GetVaultUsers } from '../../VaultUser/UseCase/GetVaultUsers'
export class ShareContactWithVault implements UseCaseInterface<void> {
constructor(
@@ -33,17 +33,18 @@ export class ShareContactWithVault implements UseCaseInterface<void> {
const users = await this.getVaultUsers.execute({
sharedVaultUuid: params.sharedVault.sharing.sharedVaultUuid,
readFromCache: false,
})
if (!users) {
if (users.isFailed()) {
return Result.fail('Cannot share contact; shared vault users not found')
}
if (users.length === 0) {
if (users.getValue().length === 0) {
return Result.ok()
}
for (const vaultUser of users) {
for (const vaultUser of users.getValue()) {
if (vaultUser.user_uuid === params.senderUserUuid) {
continue
}
@@ -1,31 +0,0 @@
import {
ClientDisplayableError,
SharedVaultInviteServerHash,
isErrorResponse,
SharedVaultPermission,
} from '@standardnotes/responses'
import { SharedVaultInvitesServerInterface } from '@standardnotes/api'
export class UpdateSharedVaultInviteUseCase {
constructor(private vaultInvitesServer: SharedVaultInvitesServerInterface) {}
async execute(params: {
sharedVaultUuid: string
inviteUuid: string
encryptedMessage: string
permissions: SharedVaultPermission
}): Promise<SharedVaultInviteServerHash | ClientDisplayableError> {
const response = await this.vaultInvitesServer.updateInvite({
sharedVaultUuid: params.sharedVaultUuid,
inviteUuid: params.inviteUuid,
encryptedMessage: params.encryptedMessage,
permissions: params.permissions,
})
if (isErrorResponse(response)) {
return ClientDisplayableError.FromNetworkError(response)
}
return response.data.invite
}
}
@@ -1,7 +1,7 @@
import { AsymmetricMessageSharedVaultInvite } from '@standardnotes/models'
import { SharedVaultInviteServerHash } from '@standardnotes/responses'
export type PendingSharedVaultInviteRecord = {
export type InviteRecord = {
invite: SharedVaultInviteServerHash
message: AsymmetricMessageSharedVaultInvite
trusted: boolean
@@ -9,7 +9,7 @@ import { SendVaultInvite } from './SendVaultInvite'
import { PkcKeyPair } from '@standardnotes/sncrypto-common'
import { EncryptMessage } from '../../Encryption/UseCase/Asymmetric/EncryptMessage'
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
import { ShareContactWithVault } from './ShareContactWithVault'
import { ShareContactWithVault } from '../../SharedVaults/UseCase/ShareContactWithVault'
import { KeySystemKeyManagerInterface } from '../../KeySystem/KeySystemKeyManagerInterface'
export class InviteToVault implements UseCaseInterface<SharedVaultInviteServerHash> {
@@ -1,4 +1,4 @@
import { DecryptOwnMessage } from './../../Encryption/UseCase/Asymmetric/DecryptOwnMessage'
import { DecryptOwnMessage } from '../../Encryption/UseCase/Asymmetric/DecryptOwnMessage'
import { AsymmetricMessageSharedVaultInvite, TrustedContactInterface } from '@standardnotes/models'
import { SharedVaultInviteServerHash } from '@standardnotes/responses'
import { PkcKeyPair } from '@standardnotes/sncrypto-common'
@@ -0,0 +1,278 @@
import { AcceptVaultInvite } from './UseCase/AcceptVaultInvite'
import { SyncEvent, SyncEventReceivedSharedVaultInvitesData } from './../Event/SyncEvent'
import { SessionEvent } from './../Session/SessionEvent'
import { InternalEventInterface } from './../Internal/InternalEventInterface'
import { InternalEventHandlerInterface } from './../Internal/InternalEventHandlerInterface'
import { ItemManagerInterface } from './../Item/ItemManagerInterface'
import { FindContact } from './../Contacts/UseCase/FindContact'
import { GetUntrustedPayload } from './../AsymmetricMessage/UseCase/GetUntrustedPayload'
import { GetTrustedPayload } from './../AsymmetricMessage/UseCase/GetTrustedPayload'
import { InviteRecord } from './InviteRecord'
import { VaultUserServiceInterface } from './../VaultUser/VaultUserServiceInterface'
import { GetVault } from './../Vaults/UseCase/GetVault'
import { InviteToVault } from './UseCase/InviteToVault'
import { GetVaultContacts } from '../VaultUser/UseCase/GetVaultContacts'
import { SyncServiceInterface } from './../Sync/SyncServiceInterface'
import { EncryptionProviderInterface } from './../Encryption/EncryptionProviderInterface'
import { InternalEventBusInterface } from './../Internal/InternalEventBusInterface'
import { SessionsClientInterface } from './../Session/SessionsClientInterface'
import { GetAllContacts } from './../Contacts/UseCase/GetAllContacts'
import {
AsymmetricMessageSharedVaultInvite,
PayloadEmitSource,
SharedVaultListingInterface,
TrustedContactInterface,
} from '@standardnotes/models'
import { VaultInviteServiceInterface } from './VaultInviteServiceInterface'
import {
ClientDisplayableError,
SharedVaultInviteServerHash,
SharedVaultPermission,
SharedVaultUserServerHash,
isErrorResponse,
} from '@standardnotes/responses'
import { AbstractService } from './../Service/AbstractService'
import { VaultInviteServiceEvent } from './VaultInviteServiceEvent'
import { ContentType, Result } from '@standardnotes/domain-core'
import { SharedVaultInvitesServer } from '@standardnotes/api'
export class VaultInviteService
extends AbstractService<VaultInviteServiceEvent>
implements VaultInviteServiceInterface, InternalEventHandlerInterface
{
private pendingInvites: Record<string, InviteRecord> = {}
constructor(
items: ItemManagerInterface,
private session: SessionsClientInterface,
private vaultUsers: VaultUserServiceInterface,
private sync: SyncServiceInterface,
private encryption: EncryptionProviderInterface,
private invitesServer: SharedVaultInvitesServer,
private _getAllContacts: GetAllContacts,
private _getVault: GetVault,
private _getVaultContacts: GetVaultContacts,
private _inviteToVault: InviteToVault,
private _getTrustedPayload: GetTrustedPayload,
private _getUntrustedPayload: GetUntrustedPayload,
private _findContact: FindContact,
private _acceptVaultInvite: AcceptVaultInvite,
eventBus: InternalEventBusInterface,
) {
super(eventBus)
this.eventDisposers.push(
items.addObserver<TrustedContactInterface>(ContentType.TYPES.TrustedContact, async ({ inserted, source }) => {
if (source === PayloadEmitSource.LocalChanged && inserted.length > 0) {
void this.downloadInboundInvites()
}
await this.reprocessCachedInvitesTrustStatusAfterTrustedContactsChange()
}),
)
}
override deinit(): void {
super.deinit()
;(this.session as unknown) = undefined
;(this.vaultUsers as unknown) = undefined
;(this.sync as unknown) = undefined
;(this.encryption as unknown) = undefined
;(this.invitesServer as unknown) = undefined
;(this._getAllContacts as unknown) = undefined
;(this._getVault as unknown) = undefined
;(this._getVaultContacts as unknown) = undefined
;(this._inviteToVault as unknown) = undefined
;(this._getTrustedPayload as unknown) = undefined
;(this._getUntrustedPayload as unknown) = undefined
;(this._findContact as unknown) = undefined
;(this._acceptVaultInvite as unknown) = undefined
this.pendingInvites = {}
}
async handleEvent(event: InternalEventInterface): Promise<void> {
switch (event.type) {
case SessionEvent.UserKeyPairChanged:
void this.invitesServer.deleteAllInboundInvites()
break
case SyncEvent.ReceivedSharedVaultInvites:
await this.processInboundInvites(event.payload as SyncEventReceivedSharedVaultInvitesData)
break
}
}
public getCachedPendingInviteRecords(): InviteRecord[] {
return Object.values(this.pendingInvites)
}
public async downloadInboundInvites(): Promise<ClientDisplayableError | SharedVaultInviteServerHash[]> {
const response = await this.invitesServer.getInboundUserInvites()
if (isErrorResponse(response)) {
return ClientDisplayableError.FromString(`Failed to get inbound user invites ${response}`)
}
this.pendingInvites = {}
await this.processInboundInvites(response.data.invites)
return response.data.invites
}
public async getOutboundInvites(
sharedVault?: SharedVaultListingInterface,
): Promise<SharedVaultInviteServerHash[] | ClientDisplayableError> {
const response = await this.invitesServer.getOutboundUserInvites()
if (isErrorResponse(response)) {
return ClientDisplayableError.FromString(`Failed to get outbound user invites ${response}`)
}
if (sharedVault) {
return response.data.invites.filter((invite) => invite.shared_vault_uuid === sharedVault.sharing.sharedVaultUuid)
}
return response.data.invites
}
public async acceptInvite(pendingInvite: InviteRecord): Promise<void> {
if (!pendingInvite.trusted) {
throw new Error('Cannot accept untrusted invite')
}
await this._acceptVaultInvite.execute({ invite: pendingInvite.invite, message: pendingInvite.message })
delete this.pendingInvites[pendingInvite.invite.uuid]
void this.sync.sync()
await this.encryption.decryptErroredPayloads()
await this.sync.syncSharedVaultsFromScratch([pendingInvite.invite.shared_vault_uuid])
}
public async getInvitableContactsForSharedVault(
sharedVault: SharedVaultListingInterface,
): Promise<TrustedContactInterface[]> {
const users = await this.vaultUsers.getSharedVaultUsers(sharedVault)
if (!users) {
return []
}
const contacts = this._getAllContacts.execute()
if (contacts.isFailed()) {
return []
}
return contacts.getValue().filter((contact) => {
const isContactAlreadyInVault = users.some((user) => user.user_uuid === contact.contactUuid)
return !isContactAlreadyInVault
})
}
public async inviteContactToSharedVault(
sharedVault: SharedVaultListingInterface,
contact: TrustedContactInterface,
permissions: SharedVaultPermission,
): Promise<Result<SharedVaultInviteServerHash>> {
const contactsResult = await this._getVaultContacts.execute({
sharedVaultUuid: sharedVault.sharing.sharedVaultUuid,
readFromCache: false,
})
if (contactsResult.isFailed()) {
return Result.fail(contactsResult.getError())
}
const contacts = contactsResult.getValue()
const result = await this._inviteToVault.execute({
keys: {
encryption: this.encryption.getKeyPair(),
signing: this.encryption.getSigningKeyPair(),
},
senderUuid: this.session.getSureUser().uuid,
sharedVault,
recipient: contact,
sharedVaultContacts: contacts,
permissions,
})
void this.notifyEvent(VaultInviteServiceEvent.InviteSent)
await this.sync.sync()
return result
}
public isVaultUserOwner(user: SharedVaultUserServerHash): boolean {
const result = this._getVault.execute<SharedVaultListingInterface>({ sharedVaultUuid: user.shared_vault_uuid })
if (result.isFailed()) {
return false
}
const vault = result.getValue()
return vault != undefined && vault.sharing.ownerUserUuid === user.user_uuid
}
public async deleteInvite(invite: SharedVaultInviteServerHash): Promise<ClientDisplayableError | void> {
const response = await this.invitesServer.deleteInvite({
sharedVaultUuid: invite.shared_vault_uuid,
inviteUuid: invite.uuid,
})
if (isErrorResponse(response)) {
return ClientDisplayableError.FromString(`Failed to delete invite ${response}`)
}
delete this.pendingInvites[invite.uuid]
}
private async reprocessCachedInvitesTrustStatusAfterTrustedContactsChange(): Promise<void> {
const cachedInvites = this.getCachedPendingInviteRecords().map((record) => record.invite)
await this.processInboundInvites(cachedInvites)
}
private async processInboundInvites(invites: SharedVaultInviteServerHash[]): Promise<void> {
if (invites.length === 0) {
return
}
for (const invite of invites) {
const sender = this._findContact.execute({ userUuid: invite.sender_uuid })
if (!sender.isFailed()) {
const trustedMessage = this._getTrustedPayload.execute<AsymmetricMessageSharedVaultInvite>({
message: invite,
privateKey: this.encryption.getKeyPair().privateKey,
ownUserUuid: this.session.userUuid,
sender: sender.getValue(),
})
if (!trustedMessage.isFailed()) {
this.pendingInvites[invite.uuid] = {
invite,
message: trustedMessage.getValue(),
trusted: true,
}
continue
}
}
const untrustedMessage = this._getUntrustedPayload.execute<AsymmetricMessageSharedVaultInvite>({
message: invite,
privateKey: this.encryption.getKeyPair().privateKey,
})
if (!untrustedMessage.isFailed()) {
this.pendingInvites[invite.uuid] = {
invite,
message: untrustedMessage.getValue(),
trusted: false,
}
}
}
void this.notifyEvent(VaultInviteServiceEvent.InvitesReloaded)
}
}
@@ -0,0 +1,4 @@
export enum VaultInviteServiceEvent {
InviteSent = 'VaultInviteServiceEvent.InviteSent',
InvitesReloaded = 'VaultInviteServiceEvent.InvitesReloaded',
}
@@ -0,0 +1,22 @@
import { InviteRecord } from './InviteRecord'
import { ApplicationServiceInterface } from '../Service/ApplicationServiceInterface'
import { SharedVaultListingInterface, TrustedContactInterface } from '@standardnotes/models'
import { ClientDisplayableError, SharedVaultInviteServerHash, SharedVaultPermission } from '@standardnotes/responses'
import { VaultInviteServiceEvent } from './VaultInviteServiceEvent'
import { Result } from '@standardnotes/domain-core'
export interface VaultInviteServiceInterface extends ApplicationServiceInterface<VaultInviteServiceEvent, unknown> {
getInvitableContactsForSharedVault(sharedVault: SharedVaultListingInterface): Promise<TrustedContactInterface[]>
inviteContactToSharedVault(
sharedVault: SharedVaultListingInterface,
contact: TrustedContactInterface,
permissions: SharedVaultPermission,
): Promise<Result<SharedVaultInviteServerHash>>
getCachedPendingInviteRecords(): InviteRecord[]
deleteInvite(invite: SharedVaultInviteServerHash): Promise<ClientDisplayableError | void>
downloadInboundInvites(): Promise<ClientDisplayableError | SharedVaultInviteServerHash[]>
getOutboundInvites(
sharedVault?: SharedVaultListingInterface,
): Promise<SharedVaultInviteServerHash[] | ClientDisplayableError>
acceptInvite(pendingInvite: InviteRecord): Promise<void>
}
@@ -7,13 +7,17 @@ import { Result, UseCaseInterface } from '@standardnotes/domain-core'
export class GetVaultContacts implements UseCaseInterface<TrustedContactInterface[]> {
constructor(private findContact: FindContact, private getVaultUsers: GetVaultUsers) {}
async execute(sharedVaultUuid: string): Promise<Result<TrustedContactInterface[]>> {
const users = await this.getVaultUsers.execute({ sharedVaultUuid })
if (!users) {
async execute(dto: { sharedVaultUuid: string; readFromCache: boolean }): Promise<Result<TrustedContactInterface[]>> {
const users = await this.getVaultUsers.execute({
sharedVaultUuid: dto.sharedVaultUuid,
readFromCache: dto.readFromCache,
})
if (users.isFailed()) {
return Result.fail('Failed to get vault users')
}
const contacts = users
.getValue()
.map((user) => this.findContact.execute({ userUuid: user.user_uuid }))
.map((result) => (result.isFailed() ? undefined : result.getValue()))
.filter(isNotUndefined)
@@ -0,0 +1,31 @@
import { VaultUserCache } from './../VaultUserCache'
import { SharedVaultUserServerHash, getErrorFromErrorResponse, isErrorResponse } from '@standardnotes/responses'
import { SharedVaultUsersServerInterface } from '@standardnotes/api'
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
export class GetVaultUsers implements UseCaseInterface<SharedVaultUserServerHash[]> {
constructor(private vaultUsersServer: SharedVaultUsersServerInterface, private cache: VaultUserCache) {}
async execute(params: {
sharedVaultUuid: string
readFromCache: boolean
}): Promise<Result<SharedVaultUserServerHash[]>> {
if (params.readFromCache) {
const cachedUsers = this.cache.get(params.sharedVaultUuid)
if (cachedUsers) {
return Result.ok(cachedUsers)
}
}
const response = await this.vaultUsersServer.getSharedVaultUsers({ sharedVaultUuid: params.sharedVaultUuid })
if (isErrorResponse(response)) {
return Result.fail(getErrorFromErrorResponse(response).message)
}
this.cache.set(params.sharedVaultUuid, response.data.users)
return Result.ok(response.data.users)
}
}
@@ -0,0 +1,12 @@
import { Result, SyncUseCaseInterface } from '@standardnotes/domain-core'
import { SharedVaultListingInterface } from '@standardnotes/models'
export class IsVaultOwner implements SyncUseCaseInterface<boolean> {
execute(dto: { sharedVault: SharedVaultListingInterface; userUuid: string }): Result<boolean> {
if (!dto.sharedVault.sharing.ownerUserUuid) {
throw new Error(`Shared vault ${dto.sharedVault.sharing.sharedVaultUuid} does not have an owner user uuid`)
}
return Result.ok(dto.sharedVault.sharing.ownerUserUuid === dto.userUuid)
}
}
@@ -1,6 +1,6 @@
import { ClientDisplayableError, isErrorResponse } from '@standardnotes/responses'
import { SharedVaultUsersServerInterface } from '@standardnotes/api'
import { DeleteThirdPartyVault } from './DeleteExternalSharedVault'
import { DeleteThirdPartyVault } from '../../SharedVaults/UseCase/DeleteExternalSharedVault'
import { ItemManagerInterface } from '../../Item/ItemManagerInterface'
import { SharedVaultListingInterface } from '@standardnotes/models'
@@ -1,17 +1,20 @@
import { ClientDisplayableError, isErrorResponse } from '@standardnotes/responses'
import { getErrorFromErrorResponse, isErrorResponse } from '@standardnotes/responses'
import { SharedVaultUsersServerInterface } from '@standardnotes/api'
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
export class RemoveVaultMember {
export class RemoveVaultMember implements UseCaseInterface<void> {
constructor(private vaultUserServer: SharedVaultUsersServerInterface) {}
async execute(params: { sharedVaultUuid: string; userUuid: string }): Promise<ClientDisplayableError | void> {
async execute(params: { sharedVaultUuid: string; userUuid: string }): Promise<Result<void>> {
const response = await this.vaultUserServer.deleteSharedVaultUser({
sharedVaultUuid: params.sharedVaultUuid,
userUuid: params.userUuid,
})
if (isErrorResponse(response)) {
return ClientDisplayableError.FromNetworkError(response)
return Result.fail(getErrorFromErrorResponse(response).message)
}
return Result.ok()
}
}
@@ -0,0 +1,15 @@
import { SharedVaultUserServerHash } from '@standardnotes/responses'
type SharedVaultUuid = string
export class VaultUserCache {
private cache = new Map<SharedVaultUuid, SharedVaultUserServerHash[]>()
public get(sharedVaultUuid: SharedVaultUuid): SharedVaultUserServerHash[] | undefined {
return this.cache.get(sharedVaultUuid)
}
public set(sharedVaultUuid: SharedVaultUuid, users: SharedVaultUserServerHash[]): void {
this.cache.set(sharedVaultUuid, users)
}
}
@@ -0,0 +1,111 @@
import { LeaveVault } from './UseCase/LeaveSharedVault'
import { GetVault } from './../Vaults/UseCase/GetVault'
import { InternalEventBusInterface } from './../Internal/InternalEventBusInterface'
import { RemoveVaultMember } from './UseCase/RemoveSharedVaultMember'
import { VaultServiceInterface } from './../Vaults/VaultServiceInterface'
import { SessionsClientInterface } from './../Session/SessionsClientInterface'
import { GetVaultUsers } from './UseCase/GetVaultUsers'
import { SharedVaultListingInterface } from '@standardnotes/models'
import { VaultUserServiceInterface } from './VaultUserServiceInterface'
import { ClientDisplayableError, SharedVaultUserServerHash, isClientDisplayableError } from '@standardnotes/responses'
import { AbstractService } from './../Service/AbstractService'
import { VaultUserServiceEvent } from './VaultUserServiceEvent'
import { Result } from '@standardnotes/domain-core'
import { IsVaultOwner } from './UseCase/IsVaultOwner'
export class VaultUserService extends AbstractService<VaultUserServiceEvent> implements VaultUserServiceInterface {
constructor(
private session: SessionsClientInterface,
private vaults: VaultServiceInterface,
private _getVaultUsers: GetVaultUsers,
private _removeVaultMember: RemoveVaultMember,
private _isVaultOwner: IsVaultOwner,
private _getVault: GetVault,
private _leaveVault: LeaveVault,
eventBus: InternalEventBusInterface,
) {
super(eventBus)
}
override deinit(): void {
super.deinit()
;(this.session as unknown) = undefined
;(this.vaults as unknown) = undefined
;(this._getVaultUsers as unknown) = undefined
;(this._removeVaultMember as unknown) = undefined
;(this._isVaultOwner as unknown) = undefined
;(this._getVault as unknown) = undefined
;(this._leaveVault as unknown) = undefined
}
public async getSharedVaultUsers(
sharedVault: SharedVaultListingInterface,
): Promise<SharedVaultUserServerHash[] | undefined> {
const result = await this._getVaultUsers.execute({
sharedVaultUuid: sharedVault.sharing.sharedVaultUuid,
readFromCache: false,
})
if (result.isFailed()) {
return undefined
}
return result.getValue()
}
public isCurrentUserSharedVaultAdmin(sharedVault: SharedVaultListingInterface): boolean {
return this._isVaultOwner
.execute({
sharedVault,
userUuid: this.session.userUuid,
})
.getValue()
}
async removeUserFromSharedVault(sharedVault: SharedVaultListingInterface, userUuid: string): Promise<Result<void>> {
if (!this.isCurrentUserSharedVaultAdmin(sharedVault)) {
throw new Error('Only vault admins can remove users')
}
if (this.vaults.isVaultLocked(sharedVault)) {
throw new Error('Cannot remove user from locked vault')
}
const result = await this._removeVaultMember.execute({
sharedVaultUuid: sharedVault.sharing.sharedVaultUuid,
userUuid,
})
if (result.isFailed()) {
return result
}
void this.notifyEvent(VaultUserServiceEvent.UsersChanged)
await this.vaults.rotateVaultRootKey(sharedVault)
return result
}
public isVaultUserOwner(user: SharedVaultUserServerHash): boolean {
const result = this._getVault.execute<SharedVaultListingInterface>({ sharedVaultUuid: user.shared_vault_uuid })
if (result.isFailed()) {
return false
}
const vault = result.getValue()
return vault != undefined && vault.sharing.ownerUserUuid === user.user_uuid
}
async leaveSharedVault(sharedVault: SharedVaultListingInterface): Promise<ClientDisplayableError | void> {
const result = await this._leaveVault.execute({
sharedVault: sharedVault,
userUuid: this.session.getSureUser().uuid,
})
if (isClientDisplayableError(result)) {
return result
}
void this.notifyEvent(VaultUserServiceEvent.UsersChanged)
}
}
@@ -0,0 +1,3 @@
export enum VaultUserServiceEvent {
UsersChanged = 'VaultUserServiceEvent.UsersChanged',
}
@@ -0,0 +1,13 @@
import { ApplicationServiceInterface } from './../Service/ApplicationServiceInterface'
import { SharedVaultListingInterface } from '@standardnotes/models'
import { ClientDisplayableError, SharedVaultUserServerHash } from '@standardnotes/responses'
import { VaultUserServiceEvent } from './VaultUserServiceEvent'
import { Result } from '@standardnotes/domain-core'
export interface VaultUserServiceInterface extends ApplicationServiceInterface<VaultUserServiceEvent, unknown> {
getSharedVaultUsers(sharedVault: SharedVaultListingInterface): Promise<SharedVaultUserServerHash[] | undefined>
isCurrentUserSharedVaultAdmin(sharedVault: SharedVaultListingInterface): boolean
removeUserFromSharedVault(sharedVault: SharedVaultListingInterface, userUuid: string): Promise<Result<void>>
leaveSharedVault(sharedVault: SharedVaultListingInterface): Promise<ClientDisplayableError | void>
isVaultUserOwner(user: SharedVaultUserServerHash): boolean
}
@@ -20,7 +20,7 @@ export class GetVault implements SyncUseCaseInterface<VaultListingInterface> {
} else {
const result = vaults.find((listing) => listing.sharing?.sharedVaultUuid === query.sharedVaultUuid) as T
if (!result) {
return Result.fail('Vault not found')
return Result.fail('Shared vault not found')
}
return Result.ok(result)
@@ -0,0 +1,15 @@
import { ContentType, Result, SyncUseCaseInterface } from '@standardnotes/domain-core'
import { VaultListingInterface } from '@standardnotes/models'
import { ItemManagerInterface } from '../../Item/ItemManagerInterface'
export class GetVaults implements SyncUseCaseInterface<VaultListingInterface[]> {
constructor(private items: ItemManagerInterface) {}
execute(): Result<VaultListingInterface[]> {
const vaults = this.items.getItems<VaultListingInterface>(ContentType.TYPES.VaultListing).sort((a, b) => {
return a.name.localeCompare(b.name)
})
return Result.ok(vaults)
}
}
@@ -28,6 +28,7 @@ import { AlertService } from '../Alert/AlertService'
import { ContentType } from '@standardnotes/domain-core'
import { EncryptionProviderInterface } from '../Encryption/EncryptionProviderInterface'
import { KeySystemKeyManagerInterface } from '../KeySystem/KeySystemKeyManagerInterface'
import { GetVaults } from './UseCase/GetVaults'
export class VaultService
extends AbstractService<VaultServiceEvent, VaultServiceEventPayload[VaultServiceEvent]>
@@ -43,11 +44,12 @@ export class VaultService
private keys: KeySystemKeyManagerInterface,
private alerts: AlertService,
private _getVault: GetVault,
private _getVaults: GetVaults,
private _changeVaultKeyOptions: ChangeVaultKeyOptions,
private _moveItemsToVault: MoveItemsToVault,
private _createVault: CreateVault,
private _removeItemFromVaultUseCase: RemoveItemFromVault,
private _deleteVaultUseCase: DeleteVault,
private _removeItemFromVault: RemoveItemFromVault,
private _deleteVaultUse: DeleteVault,
private _rotateVaultKey: RotateVaultKey,
eventBus: InternalEventBusInterface,
) {
@@ -62,9 +64,7 @@ export class VaultService
}
getVaults(): VaultListingInterface[] {
return this.items.getItems<VaultListingInterface>(ContentType.TYPES.VaultListing).sort((a, b) => {
return a.name.localeCompare(b.name)
})
return this._getVaults.execute().getValue()
}
getLockedvaults(): VaultListingInterface[] {
@@ -166,7 +166,7 @@ export class VaultService
throw new Error('Attempting to remove item from locked vault')
}
await this._removeItemFromVaultUseCase.execute({ item })
await this._removeItemFromVault.execute({ item })
return this.items.findSureItem(item.uuid)
}
@@ -175,7 +175,7 @@ export class VaultService
throw new Error('Shared vault must be deleted through SharedVaultService')
}
const error = await this._deleteVaultUseCase.execute(vault)
const error = await this._deleteVaultUse.execute(vault)
if (isClientDisplayableError(error)) {
return false
@@ -328,11 +328,12 @@ export class VaultService
;(this.encryption as unknown) = undefined
;(this.alerts as unknown) = undefined
;(this._getVault as unknown) = undefined
;(this._getVaults as unknown) = undefined
;(this._changeVaultKeyOptions as unknown) = undefined
;(this._moveItemsToVault as unknown) = undefined
;(this._createVault as unknown) = undefined
;(this._removeItemFromVaultUseCase as unknown) = undefined
;(this._deleteVaultUseCase as unknown) = undefined
;(this._removeItemFromVault as unknown) = undefined
;(this._deleteVaultUse as unknown) = undefined
;(this._rotateVaultKey as unknown) = undefined
this.lockMap.clear()
+29 -17
View File
@@ -36,6 +36,7 @@ export * from './Contacts/ContactService'
export * from './Contacts/ContactServiceInterface'
export * from './Contacts/SelfContactManager'
export * from './Contacts/UseCase/CreateOrEditContact'
export * from './Contacts/UseCase/DeleteContact'
export * from './Contacts/UseCase/EditContact'
export * from './Contacts/UseCase/FindContact'
export * from './Contacts/UseCase/GetAllContacts'
@@ -57,15 +58,15 @@ export * from './Device/MobileDeviceInterface'
export * from './Device/TypeCheck'
export * from './Device/WebOrDesktopDeviceInterface'
export * from './Diagnostics/ServiceDiagnostics'
export * from './Encryption/UseCase/DecryptBackupFile'
export * from './Encryption/EncryptionService'
export * from './Encryption/EncryptionProviderInterface'
export * from './Encryption/EncryptionService'
export * from './Encryption/EncryptionServiceEvent'
export * from './Encryption/Functions'
export * from './Encryption/UseCase/Asymmetric/DecryptMessage'
export * from './Encryption/UseCase/Asymmetric/DecryptOwnMessage'
export * from './Encryption/UseCase/Asymmetric/EncryptMessage'
export * from './Encryption/UseCase/Asymmetric/GetMessageAdditionalData'
export * from './Encryption/UseCase/DecryptBackupFile'
export * from './Encryption/UseCase/ItemsKey/CreateNewDefaultItemsKey'
export * from './Encryption/UseCase/ItemsKey/CreateNewItemsKeyWithRollback'
export * from './Encryption/UseCase/ItemsKey/FindDefaultItemsKey'
@@ -111,6 +112,7 @@ export * from './Item/ItemRelationshipDirection'
export * from './Item/ItemsServerInterface'
export * from './Item/StaticItemCounter'
export * from './ItemsEncryption/ItemsEncryption'
export * from './ItemsEncryption/ItemsEncryption'
export * from './KeySystem/KeySystemKeyManager'
export * from './Mutator/ImportDataUseCase'
export * from './Mutator/MutatorClientInterface'
@@ -121,39 +123,28 @@ export * from './Protection/ProtectionClientInterface'
export * from './Protection/TimingDisplayOption'
export * from './Revision/RevisionClientInterface'
export * from './Revision/RevisionManager'
export * from './RootKeyManager/RootKeyManager'
export * from './RootKeyManager/KeyMode'
export * from './ItemsEncryption/ItemsEncryption'
export * from './RootKeyManager/RootKeyManager'
export * from './Service/AbstractService'
export * from './Service/ApplicationServiceInterface'
export * from './Session/SessionEvent'
export * from './Session/SessionManagerResponse'
export * from './Session/SessionsClientInterface'
export * from './Session/UserKeyPairChangedEventData'
export * from './SharedVaults/PendingSharedVaultInviteRecord'
export * from './SharedVaults/SharedVaultService'
export * from './SharedVaults/SharedVaultServiceEvent'
export * from './SharedVaults/SharedVaultServiceInterface'
export * from './SharedVaults/UseCase/AcceptVaultInvite'
export * from './SharedVaults/UseCase/ContactBelongsToVault'
export * from './SharedVaults/UseCase/ConvertToSharedVault'
export * from './SharedVaults/UseCase/CreateSharedVault'
export * from './SharedVaults/UseCase/DeleteExternalSharedVault'
export * from './SharedVaults/UseCase/DeleteSharedVault'
export * from './SharedVaults/UseCase/GetVaultContacts'
export * from './SharedVaults/UseCase/GetVaultContacts'
export * from './SharedVaults/UseCase/GetVaultUsers'
export * from './SharedVaults/UseCase/InviteToVault'
export * from './SharedVaults/UseCase/LeaveSharedVault'
export * from './SharedVaults/UseCase/GetOwnedSharedVaults'
export * from './SharedVaults/UseCase/GetSharedVaults'
export * from './SharedVaults/UseCase/NotifyVaultUsersOfKeyRotation'
export * from './SharedVaults/UseCase/RemoveSharedVaultMember'
export * from './SharedVaults/UseCase/ReuploadAllInvites'
export * from './SharedVaults/UseCase/ReuploadInvite'
export * from './SharedVaults/UseCase/ReuploadVaultInvites'
export * from './SharedVaults/UseCase/SendVaultDataChangedMessage'
export * from './SharedVaults/UseCase/SendVaultInvite'
export * from './SharedVaults/UseCase/SendVaultKeyChangedMessage'
export * from './SharedVaults/UseCase/ShareContactWithVault'
export * from './SharedVaults/UseCase/UpdateSharedVaultInvite'
export * from './Singleton/SingletonManagerInterface'
export * from './Status/StatusService'
export * from './Status/StatusServiceInterface'
@@ -186,14 +177,35 @@ export * from './User/UserClientInterface'
export * from './User/UserService'
export * from './UserEvent/UserEventService'
export * from './UserEvent/UserEventServiceEvent'
export * from './VaultInvite/InviteRecord'
export * from './VaultInvite/UseCase/AcceptVaultInvite'
export * from './VaultInvite/UseCase/InviteToVault'
export * from './VaultInvite/UseCase/ReuploadAllInvites'
export * from './VaultInvite/UseCase/ReuploadInvite'
export * from './VaultInvite/UseCase/ReuploadVaultInvites'
export * from './VaultInvite/UseCase/SendVaultInvite'
export * from './VaultInvite/VaultInviteService'
export * from './VaultInvite/VaultInviteServiceEvent'
export * from './VaultInvite/VaultInviteServiceInterface'
export * from './Vaults/ChangeVaultOptionsDTO'
export * from './Vaults/UseCase/ChangeVaultKeyOptions'
export * from './Vaults/UseCase/CreateVault'
export * from './Vaults/UseCase/DeleteVault'
export * from './Vaults/UseCase/GetVault'
export * from './Vaults/UseCase/GetVaults'
export * from './Vaults/UseCase/MoveItemsToVault'
export * from './Vaults/UseCase/RemoveItemFromVault'
export * from './Vaults/UseCase/RotateVaultKey'
export * from './Vaults/VaultService'
export * from './Vaults/VaultServiceEvent'
export * from './Vaults/VaultServiceInterface'
export * from './VaultUser/UseCase/GetVaultContacts'
export * from './VaultUser/UseCase/GetVaultContacts'
export * from './VaultUser/UseCase/GetVaultUsers'
export * from './VaultUser/UseCase/IsVaultOwner'
export * from './VaultUser/UseCase/LeaveSharedVault'
export * from './VaultUser/UseCase/RemoveSharedVaultMember'
export * from './VaultUser/VaultUserCache'
export * from './VaultUser/VaultUserService'
export * from './VaultUser/VaultUserServiceEvent'
export * from './VaultUser/VaultUserServiceInterface'
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.202.12](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-24)
**Note:** Version bump only for package @standardnotes/snjs
## [2.202.11](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-24)
**Note:** Version bump only for package @standardnotes/snjs
## [2.202.10](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-23)
**Note:** Version bump only for package @standardnotes/snjs
+29 -9
View File
@@ -71,6 +71,10 @@ import {
HistoryServiceInterface,
InternalEventPublishStrategy,
EncryptionProviderInterface,
VaultUserServiceInterface,
VaultInviteServiceInterface,
UserEventServiceEvent,
VaultServiceEvent,
} from '@standardnotes/services'
import {
PayloadEmitSource,
@@ -1130,7 +1134,15 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
this.events.addEventHandler(this.dependencies.get(TYPES.UserService), AccountEvent.SignedInOrRegistered)
this.events.addEventHandler(this.dependencies.get(TYPES.SessionManager), ApiServiceEvent.SessionRefreshed)
this.events.addEventHandler(this.dependencies.get(TYPES.SharedVaultService), SyncEvent.ReceivedSharedVaultInvites)
this.events.addEventHandler(this.dependencies.get(TYPES.VaultInviteService), SyncEvent.ReceivedSharedVaultInvites)
this.events.addEventHandler(this.dependencies.get(TYPES.VaultInviteService), SessionEvent.UserKeyPairChanged)
this.events.addEventHandler(this.dependencies.get(TYPES.SharedVaultService), SessionEvent.UserKeyPairChanged)
this.events.addEventHandler(
this.dependencies.get(TYPES.SharedVaultService),
UserEventServiceEvent.UserEventReceived,
)
this.events.addEventHandler(this.dependencies.get(TYPES.SharedVaultService), VaultServiceEvent.VaultRootKeyRotated)
this.events.addEventHandler(this.dependencies.get(TYPES.SharedVaultService), SyncEvent.ReceivedRemoteSharedVaults)
this.events.addEventHandler(
@@ -1332,14 +1344,26 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
return this.dependencies.get<HistoryServiceInterface>(TYPES.HistoryManager)
}
private get migrations(): MigrationService {
return this.dependencies.get<MigrationService>(TYPES.MigrationService)
}
public get encryption(): EncryptionProviderInterface {
return this.dependencies.get<EncryptionProviderInterface>(TYPES.EncryptionService)
}
public get events(): InternalEventBusInterface {
return this.dependencies.get<InternalEventBusInterface>(TYPES.InternalEventBus)
}
public get vaultUsers(): VaultUserServiceInterface {
return this.dependencies.get<VaultUserServiceInterface>(TYPES.VaultUserService)
}
public get vaultInvites(): VaultInviteServiceInterface {
return this.dependencies.get<VaultInviteServiceInterface>(TYPES.VaultInviteService)
}
private get migrations(): MigrationService {
return this.dependencies.get<MigrationService>(TYPES.MigrationService)
}
private get legacyApi(): LegacyApiService {
return this.dependencies.get<LegacyApiService>(TYPES.LegacyApiService)
}
@@ -1352,10 +1376,6 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
return this.dependencies.get<WebSocketsService>(TYPES.WebSocketsService)
}
public get events(): InternalEventBusInterface {
return this.dependencies.get<InternalEventBusInterface>(TYPES.InternalEventBus)
}
private get mfa(): SNMfaService {
return this.dependencies.get<SNMfaService>(TYPES.MfaService)
}
@@ -108,6 +108,15 @@ import {
RootKeyManager,
ItemsEncryptionService,
DecryptBackupFile,
VaultUserService,
IsVaultOwner,
VaultInviteService,
VaultUserCache,
GetVaults,
GetSharedVaults,
GetOwnedSharedVaults,
ContactBelongsToVault,
DeleteContact,
} from '@standardnotes/services'
import { ItemManager } from '../../Services/Items/ItemManager'
import { PayloadManager } from '../../Services/Payloads/PayloadManager'
@@ -204,6 +213,10 @@ export class Dependencies {
)
})
this.factory.set(TYPES.IsVaultOwner, () => {
return new IsVaultOwner()
})
this.factory.set(TYPES.DecryptBackupFile, () => {
return new DecryptBackupFile(this.get(TYPES.EncryptionService))
})
@@ -216,6 +229,15 @@ export class Dependencies {
return new FindContact(this.get(TYPES.ItemManager))
})
this.factory.set(TYPES.DeleteContact, () => {
return new DeleteContact(
this.get(TYPES.MutatorService),
this.get(TYPES.SyncService),
this.get(TYPES.GetOwnedSharedVaults),
this.get(TYPES.ContactBelongsToVault),
)
})
this.factory.set(TYPES.EditContact, () => {
return new EditContact(this.get(TYPES.MutatorService), this.get(TYPES.SyncService))
})
@@ -241,6 +263,22 @@ export class Dependencies {
return new GetVault(this.get(TYPES.ItemManager))
})
this.factory.set(TYPES.GetVaults, () => {
return new GetVaults(this.get(TYPES.ItemManager))
})
this.factory.set(TYPES.GetSharedVaults, () => {
return new GetSharedVaults(this.get(TYPES.GetVaults))
})
this.factory.set(TYPES.GetOwnedSharedVaults, () => {
return new GetOwnedSharedVaults(this.get(TYPES.GetSharedVaults), this.get(TYPES.IsVaultOwner))
})
this.factory.set(TYPES.ContactBelongsToVault, () => {
return new ContactBelongsToVault(this.get(TYPES.GetVaultUsers))
})
this.factory.set(TYPES.ChangeVaultKeyOptions, () => {
return new ChangeVaultKeyOptions(
this.get(TYPES.MutatorService),
@@ -446,7 +484,7 @@ export class Dependencies {
})
this.factory.set(TYPES.GetVaultUsers, () => {
return new GetVaultUsers(this.get(TYPES.SharedVaultUsersServer))
return new GetVaultUsers(this.get(TYPES.SharedVaultUsersServer), this.get(TYPES.VaultUserCache))
})
this.factory.set(TYPES.DecryptOwnMessage, () => {
@@ -608,11 +646,49 @@ export class Dependencies {
return new SharedVaultUsersServer(this.get(TYPES.HttpService))
})
this.factory.set(TYPES.VaultUserService, () => {
return new VaultUserService(
this.get(TYPES.SessionManager),
this.get(TYPES.VaultService),
this.get(TYPES.GetVaultUsers),
this.get(TYPES.RemoveVaultMember),
this.get(TYPES.IsVaultOwner),
this.get(TYPES.GetVault),
this.get(TYPES.LeaveVault),
this.get(TYPES.InternalEventBus),
)
})
this.factory.set(TYPES.VaultUserCache, () => {
return new VaultUserCache()
})
this.factory.set(TYPES.VaultInviteService, () => {
return new VaultInviteService(
this.get(TYPES.ItemManager),
this.get(TYPES.SessionManager),
this.get(TYPES.VaultUserService),
this.get(TYPES.SyncService),
this.get(TYPES.EncryptionService),
this.get(TYPES.SharedVaultInvitesServer),
this.get(TYPES.GetAllContacts),
this.get(TYPES.GetVault),
this.get(TYPES.GetVaultContacts),
this.get(TYPES.InviteToVault),
this.get(TYPES.GetTrustedPayload),
this.get(TYPES.GetUntrustedPayload),
this.get(TYPES.FindContact),
this.get(TYPES.AcceptVaultInvite),
this.get(TYPES.InternalEventBus),
)
})
this.factory.set(TYPES.AsymmetricMessageService, () => {
return new AsymmetricMessageService(
this.get(TYPES.AsymmetricMessageServer),
this.get(TYPES.EncryptionService),
this.get(TYPES.MutatorService),
this.get(TYPES.SessionManager),
this.get(TYPES.AsymmetricMessageServer),
this.get(TYPES.CreateOrEditContact),
this.get(TYPES.FindContact),
this.get(TYPES.GetAllContacts),
@@ -630,31 +706,21 @@ export class Dependencies {
this.factory.set(TYPES.SharedVaultService, () => {
return new SharedVaultService(
this.get(TYPES.SyncService),
this.get(TYPES.ItemManager),
this.get(TYPES.EncryptionService),
this.get(TYPES.SessionManager),
this.get(TYPES.VaultService),
this.get(TYPES.SharedVaultInvitesServer),
this.get(TYPES.GetVault),
this.get(TYPES.GetOwnedSharedVaults),
this.get(TYPES.CreateSharedVault),
this.get(TYPES.HandleKeyPairChange),
this.get(TYPES.NotifyVaultUsersOfKeyRotation),
this.get(TYPES.SendVaultDataChangedMessage),
this.get(TYPES.GetTrustedPayload),
this.get(TYPES.GetUntrustedPayload),
this.get(TYPES.FindContact),
this.get(TYPES.GetAllContacts),
this.get(TYPES.GetVaultContacts),
this.get(TYPES.AcceptVaultInvite),
this.get(TYPES.InviteToVault),
this.get(TYPES.LeaveVault),
this.get(TYPES.DeleteThirdPartyVault),
this.get(TYPES.ShareContactWithVault),
this.get(TYPES.ConvertToSharedVault),
this.get(TYPES.DeleteSharedVault),
this.get(TYPES.RemoveVaultMember),
this.get(TYPES.GetVaultUsers),
this.get(TYPES.IsVaultOwner),
this.get(TYPES.InternalEventBus),
)
})
@@ -668,6 +734,7 @@ export class Dependencies {
this.get(TYPES.KeySystemKeyManager),
this.get(TYPES.AlertService),
this.get(TYPES.GetVault),
this.get(TYPES.GetVaults),
this.get(TYPES.ChangeVaultKeyOptions),
this.get(TYPES.MoveItemsToVault),
this.get(TYPES.CreateVault),
@@ -697,6 +764,7 @@ export class Dependencies {
this.get(TYPES.UserService),
this.get(TYPES.SelfContactManager),
this.get(TYPES.EncryptionService),
this.get(TYPES.DeleteContact),
this.get(TYPES.FindContact),
this.get(TYPES.GetAllContacts),
this.get(TYPES.CreateOrEditContact),
@@ -59,6 +59,9 @@ export const TYPES = {
EncryptionOperators: Symbol.for('EncryptionOperators'),
RootKeyManager: Symbol.for('RootKeyManager'),
ItemsEncryptionService: Symbol.for('ItemsEncryptionService'),
VaultUserService: Symbol.for('VaultUserService'),
VaultInviteService: Symbol.for('VaultInviteService'),
VaultUserCache: Symbol.for('VaultUserCache'),
// Servers
RevisionServer: Symbol.for('RevisionServer'),
@@ -92,9 +95,14 @@ export const TYPES = {
EditContact: Symbol.for('EditContact'),
ValidateItemSigner: Symbol.for('ValidateItemSigner'),
GetVault: Symbol.for('GetVault'),
GetVaults: Symbol.for('GetVaults'),
GetSharedVaults: Symbol.for('GetSharedVaults'),
GetOwnedSharedVaults: Symbol.for('GetOwnedSharedVaults'),
ChangeVaultKeyOptions: Symbol.for('ChangeVaultKeyOptions'),
MoveItemsToVault: Symbol.for('MoveItemsToVault'),
CreateVault: Symbol.for('CreateVault'),
DeleteContact: Symbol.for('DeleteContact'),
ContactBelongsToVault: Symbol.for('ContactBelongsToVault'),
RemoveItemFromVault: Symbol.for('RemoveItemFromVault'),
DeleteVault: Symbol.for('DeleteVault'),
RotateVaultKey: Symbol.for('RotateVaultKey'),
@@ -140,6 +148,7 @@ export const TYPES = {
EncryptTypeAPayload: Symbol.for('EncryptTypeAPayload'),
EncryptTypeAPayloadWithKeyLookup: Symbol.for('EncryptTypeAPayloadWithKeyLookup'),
DecryptBackupFile: Symbol.for('DecryptBackupFile'),
IsVaultOwner: Symbol.for('IsVaultOwner'),
// Mappers
SessionStorageMapper: Symbol.for('SessionStorageMapper'),
+21 -18
View File
@@ -1,18 +1,21 @@
export const VaultTests = [
'vaults/vaults.test.js',
'vaults/pkc.test.js',
'vaults/contacts.test.js',
'vaults/crypto.test.js',
'vaults/asymmetric-messages.test.js',
'vaults/keypair-change.test.js',
'vaults/signatures.test.js',
'vaults/shared_vaults.test.js',
'vaults/invites.test.js',
'vaults/items.test.js',
'vaults/conflicts.test.js',
'vaults/deletion.test.js',
'vaults/permissions.test.js',
'vaults/key_rotation.test.js',
'vaults/files.test.js',
];
export const VaultTests = {
enabled: false,
exclusive: false,
files: [
'vaults/vaults.test.js',
'vaults/pkc.test.js',
'vaults/contacts.test.js',
'vaults/crypto.test.js',
'vaults/asymmetric-messages.test.js',
'vaults/keypair-change.test.js',
'vaults/signatures.test.js',
'vaults/shared_vaults.test.js',
'vaults/invites.test.js',
'vaults/items.test.js',
'vaults/conflicts.test.js',
'vaults/deletion.test.js',
'vaults/permissions.test.js',
'vaults/key_rotation.test.js',
'vaults/files.test.js',
],
}
+12 -12
View File
@@ -97,6 +97,14 @@ export class AppContext {
return this.application.sharedVaults
}
get vaultUsers() {
return this.application.vaultUsers
}
get vaultInvites() {
return this.application.vaultInvites
}
get files() {
return this.application.files
}
@@ -210,16 +218,6 @@ export class AppContext {
})
}
resolveWhenSharedVaultUserKeysResolved() {
return new Promise((resolve) => {
this.application.vaults.collaboration.addEventObserver((eventName) => {
if (eventName === SharedVaultServiceEvent.SharedVaultStatusChanged) {
resolve()
}
})
})
}
async awaitSignInEvent() {
return new Promise((resolve) => {
this.application.user.addEventObserver((eventName) => {
@@ -394,7 +392,7 @@ export class AppContext {
resolveWhenAllInboundSharedVaultInvitesAreDeleted() {
return new Promise((resolve) => {
const objectToSpy = this.application.sharedVaults.invitesServer
const objectToSpy = this.application.vaultInvites.invitesServer
sinon.stub(objectToSpy, 'deleteAllInboundInvites').callsFake(async (params) => {
objectToSpy.deleteAllInboundInvites.restore()
const result = await objectToSpy.deleteAllInboundInvites(params)
@@ -580,9 +578,11 @@ export class AppContext {
}
async changeNoteTitle(note, title) {
return this.application.mutator.changeNote(note, (mutator) => {
await this.application.mutator.changeNote(note, (mutator) => {
mutator.title = title
})
return this.findItem(note.uuid)
}
async changeNoteTitleAndSync(note, title) {
+5 -5
View File
@@ -26,13 +26,13 @@ export const createTrustedContactForUserOfContext = async (
}
export const acceptAllInvites = async (context) => {
const inviteRecords = context.sharedVaults.getCachedPendingInviteRecords()
const inviteRecords = context.vaultInvites.getCachedPendingInviteRecords()
if (inviteRecords.length === 0) {
throw new Error('No pending invites to accept')
}
for (const record of inviteRecords) {
await context.sharedVaults.acceptPendingSharedVaultInvite(record)
await context.vaultInvites.acceptInvite(record)
}
}
@@ -76,7 +76,7 @@ export const createSharedVaultWithUnacceptedButTrustedInvite = async (
const contact = await createTrustedContactForUserOfContext(context, contactContext)
await createTrustedContactForUserOfContext(contactContext, context)
const invite = (await context.sharedVaults.inviteContactToSharedVault(sharedVault, contact, permissions)).getValue()
const invite = (await context.vaultInvites.inviteContactToSharedVault(sharedVault, contact, permissions)).getValue()
await contactContext.sync()
return { sharedVault, contact, contactContext, deinitContactContext, invite }
@@ -91,7 +91,7 @@ export const createSharedVaultWithUnacceptedAndUntrustedInvite = async (
const { contactContext, deinitContactContext } = await createContactContext()
const contact = await createTrustedContactForUserOfContext(context, contactContext)
const invite = (await context.sharedVaults.inviteContactToSharedVault(sharedVault, contact, permissions)).getValue()
const invite = (await context.vaultInvites.inviteContactToSharedVault(sharedVault, contact, permissions)).getValue()
await contactContext.sync()
return { sharedVault, contact, contactContext, deinitContactContext, invite }
@@ -103,7 +103,7 @@ export const inviteNewPartyToSharedVault = async (context, sharedVault, permissi
const thirdPartyContact = await createTrustedContactForUserOfContext(context, thirdPartyContext)
await createTrustedContactForUserOfContext(thirdPartyContext, context)
await context.sharedVaults.inviteContactToSharedVault(sharedVault, thirdPartyContact, permissions)
await context.vaultInvites.inviteContactToSharedVault(sharedVault, thirdPartyContact, permissions)
await thirdPartyContext.sync()
+4 -8
View File
@@ -38,10 +38,6 @@
<script type="module">
import MainRegistry from './TestRegistry/MainRegistry.js'
const InternalFeatureStatus = {
[InternalFeature.Vaults]: { enabled: false, exclusive: false },
}
const loadTest = (fileName) => {
return new Promise((resolve) => {
const script = document.createElement('script');
@@ -60,12 +56,12 @@
}
}
if (InternalFeatureStatus[InternalFeature.Vaults].enabled) {
if (MainRegistry.VaultTests.enabled) {
InternalFeatureService.get().enableFeature(InternalFeature.Vaults);
await loadTests(MainRegistry.VaultTests);
await loadTests(MainRegistry.VaultTests.files);
}
if (!InternalFeatureStatus[InternalFeature.Vaults].exclusive) {
if (!MainRegistry.VaultTests.enabled.exclusive) {
await loadTests(MainRegistry.BaseTests);
}
@@ -77,4 +73,4 @@
<div id="mocha"></div>
</body>
</html>
</html>
@@ -27,7 +27,32 @@ describe('asymmetric messages', function () {
})
it('should not trust message if the trusted payload data recipientUuid does not match the message user uuid', async () => {
console.error('TODO: implement')
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context)
contactContext.lockSyncing()
await context.vaults.changeVaultNameAndDescription(sharedVault, {
name: 'new vault name',
description: 'new vault description',
})
Object.defineProperty(contactContext.asymmetric.sessions, 'userUuid', {
get: () => 'invalid user uuid',
})
const completedProcessingMessagesPromise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes()
contactContext.unlockSyncing()
await contactContext.sync()
await completedProcessingMessagesPromise
const updatedVault = contactContext.vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier })
expect(updatedVault.name).to.not.equal('new vault name')
expect(updatedVault.description).to.not.equal('new vault description')
await deinitContactContext()
})
it('should delete message after processing it', async () => {
@@ -89,7 +114,7 @@ describe('asymmetric messages', function () {
await contactContext.sync()
await completedProcessingMessagesPromise
const updatedContact = contactContext.contacts.findTrustedContact(thirdPartyContext.userUuid)
const updatedContact = contactContext.contacts.findContact(thirdPartyContext.userUuid)
expect(updatedContact.name).to.equal('Changed 3rd Party Name')
await deinitContactContext()
@@ -228,7 +253,7 @@ describe('asymmetric messages', function () {
expect(firstPartySpy.callCount).to.equal(0)
expect(secondPartySpy.callCount).to.equal(1)
const contact = contactContext.contacts.findTrustedContact(context.userUuid)
const contact = contactContext.contacts.findContact(context.userUuid)
expect(contact.publicKeySet.encryption).to.equal(context.publicKey)
expect(contact.publicKeySet.signing).to.equal(context.signingPublicKey)
@@ -294,7 +319,7 @@ describe('asymmetric messages', function () {
const { contactContext, deinitContactContext } = await Collaboration.createContactContext()
await Collaboration.createTrustedContactForUserOfContext(context, contactContext)
await Collaboration.createTrustedContactForUserOfContext(contactContext, context)
const originalContact = contactContext.contacts.findTrustedContact(context.userUuid)
const originalContact = contactContext.contacts.findContact(context.userUuid)
await context.changePassword('new_password')
@@ -302,7 +327,7 @@ describe('asymmetric messages', function () {
await contactContext.sync()
await completedProcessingMessagesPromise
const updatedContact = contactContext.contacts.findTrustedContact(context.userUuid)
const updatedContact = contactContext.contacts.findContact(context.userUuid)
expect(updatedContact.publicKeySet.encryption).to.not.equal(originalContact.publicKeySet.encryption)
expect(updatedContact.publicKeySet.signing).to.not.equal(originalContact.publicKeySet.signing)
@@ -352,7 +377,7 @@ describe('asymmetric messages', function () {
await contactContext.sync()
await completedProcessingMessagesPromise
const updatedContact = contactContext.contacts.findTrustedContact(context.userUuid)
const updatedContact = contactContext.contacts.findContact(context.userUuid)
expect(updatedContact.publicKeySet.encryption).to.equal(newKeyPair.publicKey)
expect(updatedContact.publicKeySet.signing).to.equal(newSigningKeyPair.publicKey)
+2 -2
View File
@@ -28,7 +28,7 @@ describe('shared vault conflicts', function () {
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
contactContext.lockSyncing()
await context.sharedVaults.removeUserFromSharedVault(sharedVault, contactContext.userUuid)
await context.vaultUsers.removeUserFromSharedVault(sharedVault, contactContext.userUuid)
const promise = contactContext.resolveWithConflicts()
contactContext.unlockSyncing()
await contactContext.changeNoteTitleAndSync(note, 'new title')
@@ -98,7 +98,7 @@ describe('shared vault conflicts', function () {
const { sharedVault, note, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
await context.sharedVaults.removeUserFromSharedVault(sharedVault, contactContext.userUuid)
await context.vaultUsers.removeUserFromSharedVault(sharedVault, contactContext.userUuid)
await contactContext.changeNoteTitleAndSync(note, 'new title')
const notes = contactContext.notes
+9 -1
View File
@@ -1,4 +1,5 @@
import * as Factory from '../lib/factory.js'
import * as Collaboration from '../lib/Collaboration.js'
chai.use(chaiAsPromised)
const expect = chai.expect
@@ -90,7 +91,14 @@ describe('contacts', function () {
})
it('should not be able to delete a trusted contact if it belongs to a vault I administer', async () => {
console.error('TODO: implement test')
const { contact, deinitContactContext } = await Collaboration.createSharedVaultWithAcceptedInvite(context)
const result = await context.contacts.deleteContact(contact)
expect(result.isFailed()).to.be.true
expect(result.getError()).to.equal('Cannot delete contact that belongs to an owned vault')
await deinitContactContext()
})
it('should be able to refresh a contact using a collaborationID that includes full chain of previouos public keys', async () => {
+3 -3
View File
@@ -167,11 +167,11 @@ describe('shared vault crypto', function () {
const { note, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
expect(context.contacts.isItemAuthenticallySigned(note)).to.equal(ItemSignatureValidationResult.NotApplicable)
expect(context.contacts.getItemSignatureStatus(note)).to.equal(ItemSignatureValidationResult.NotApplicable)
const contactNote = contactContext.items.findItem(note.uuid)
expect(contactContext.contacts.isItemAuthenticallySigned(contactNote)).to.equal(
expect(contactContext.contacts.getItemSignatureStatus(contactNote)).to.equal(
ItemSignatureValidationResult.Trusted,
)
@@ -181,7 +181,7 @@ describe('shared vault crypto', function () {
let updatedNote = context.items.findItem(note.uuid)
expect(context.contacts.isItemAuthenticallySigned(updatedNote)).to.equal(ItemSignatureValidationResult.Trusted)
expect(context.contacts.getItemSignatureStatus(updatedNote)).to.equal(ItemSignatureValidationResult.Trusted)
await deinitContactContext()
})
+6 -7
View File
@@ -90,7 +90,7 @@ describe('shared vault deletion', function () {
const contactNote = contactContext.items.findItem(note.uuid)
expect(contactNote).to.not.be.undefined
await context.sharedVaults.removeUserFromSharedVault(sharedVault, contactContext.userUuid)
await context.vaultUsers.removeUserFromSharedVault(sharedVault, contactContext.userUuid)
await contactContext.sync()
@@ -108,7 +108,7 @@ describe('shared vault deletion', function () {
expect(originalNote).to.not.be.undefined
const contactVault = contactContext.vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier })
await contactContext.sharedVaults.leaveSharedVault(contactVault)
await contactContext.vaultUsers.leaveSharedVault(contactVault)
const updatedContactNote = contactContext.items.findItem(note.uuid)
expect(updatedContactNote).to.be.undefined
@@ -140,14 +140,13 @@ describe('shared vault deletion', function () {
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context)
const originalSharedVaultUsers = await sharedVaults.getSharedVaultUsers(sharedVault)
const originalSharedVaultUsers = await context.vaultUsers.getSharedVaultUsers(sharedVault)
expect(originalSharedVaultUsers.length).to.equal(2)
const result = await sharedVaults.removeUserFromSharedVault(sharedVault, contactContext.userUuid)
const result = await context.vaultUsers.removeUserFromSharedVault(sharedVault, contactContext.userUuid)
expect(result.isFailed()).to.be.false
expect(isClientDisplayableError(result)).to.be.false
const updatedSharedVaultUsers = await sharedVaults.getSharedVaultUsers(sharedVault)
const updatedSharedVaultUsers = await context.vaultUsers.getSharedVaultUsers(sharedVault)
expect(updatedSharedVaultUsers.length).to.equal(1)
await deinitContactContext()
+1 -1
View File
@@ -249,7 +249,7 @@ describe('shared vault files', function () {
const uploadedFile = await Files.uploadFile(context.files, buffer, 'my-file', 'md', 1000, sharedVault)
await contactContext.sync()
await context.sharedVaults.removeUserFromSharedVault(sharedVault, contactContext.userUuid)
await context.vaultUsers.removeUserFromSharedVault(sharedVault, contactContext.userUuid)
const file = contactContext.items.findItem(uploadedFile.uuid)
await Factory.expectThrowsAsync(() => Files.downloadFile(contactContext.files, file), 'Could not download file')
+24 -19
View File
@@ -8,7 +8,6 @@ describe('shared vault invites', function () {
this.timeout(Factory.TwentySecondTimeout)
let context
let sharedVaults
afterEach(async function () {
await context.deinit()
@@ -21,8 +20,6 @@ describe('shared vault invites', function () {
context = await Factory.createAppContextWithRealCrypto()
await context.launch()
await context.register()
sharedVaults = context.sharedVaults
})
it('should invite contact to vault', async () => {
@@ -31,7 +28,7 @@ describe('shared vault invites', function () {
const contact = await Collaboration.createTrustedContactForUserOfContext(context, contactContext)
const vaultInvite = (
await sharedVaults.inviteContactToSharedVault(sharedVault, contact, SharedVaultPermission.Write)
await context.vaultInvites.inviteContactToSharedVault(sharedVault, contact, SharedVaultPermission.Write)
).getValue()
expect(vaultInvite).to.not.be.undefined
@@ -49,7 +46,7 @@ describe('shared vault invites', function () {
const { contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithUnacceptedButTrustedInvite(context)
const invites = contactContext.sharedVaults.getCachedPendingInviteRecords()
const invites = contactContext.vaultInvites.getCachedPendingInviteRecords()
expect(invites[0].trusted).to.be.true
@@ -60,7 +57,7 @@ describe('shared vault invites', function () {
const { contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithUnacceptedAndUntrustedInvite(context)
const invites = contactContext.sharedVaults.getCachedPendingInviteRecords()
const invites = contactContext.vaultInvites.getCachedPendingInviteRecords()
expect(invites[0].trusted).to.be.false
@@ -76,7 +73,7 @@ describe('shared vault invites', function () {
sharedVault,
)
const invites = thirdPartyContext.sharedVaults.getCachedPendingInviteRecords()
const invites = thirdPartyContext.vaultInvites.getCachedPendingInviteRecords()
const message = invites[0].message
const delegatedContacts = message.data.trustedContacts
@@ -103,7 +100,7 @@ describe('shared vault invites', function () {
/** Sync the contact context so that they wouldn't naturally receive changes made before this point */
await contactContext.sync()
await sharedVaults.inviteContactToSharedVault(sharedVault, contact, SharedVaultPermission.Write)
await context.vaultInvites.inviteContactToSharedVault(sharedVault, contact, SharedVaultPermission.Write)
/** Contact should now sync and expect to find note */
const promise = contactContext.awaitNextSyncSharedVaultFromScratchEvent()
@@ -125,10 +122,14 @@ describe('shared vault invites', function () {
const sharedVault = await Collaboration.createSharedVault(context)
const currentContextContact = await Collaboration.createTrustedContactForUserOfContext(context, contactContext)
await sharedVaults.inviteContactToSharedVault(sharedVault, currentContextContact, SharedVaultPermission.Write)
await context.vaultInvites.inviteContactToSharedVault(
sharedVault,
currentContextContact,
SharedVaultPermission.Write,
)
await contactContext.sharedVaults.downloadInboundInvites()
expect(contactContext.sharedVaults.getCachedPendingInviteRecords()[0].trusted).to.be.false
await contactContext.vaultInvites.downloadInboundInvites()
expect(contactContext.vaultInvites.getCachedPendingInviteRecords()[0].trusted).to.be.false
await deinitContactContext()
})
@@ -139,14 +140,18 @@ describe('shared vault invites', function () {
const sharedVault = await Collaboration.createSharedVault(context)
const currentContextContact = await Collaboration.createTrustedContactForUserOfContext(context, contactContext)
await sharedVaults.inviteContactToSharedVault(sharedVault, currentContextContact, SharedVaultPermission.Write)
await context.vaultInvites.inviteContactToSharedVault(
sharedVault,
currentContextContact,
SharedVaultPermission.Write,
)
await contactContext.sharedVaults.downloadInboundInvites()
expect(contactContext.sharedVaults.getCachedPendingInviteRecords()[0].trusted).to.be.false
await contactContext.vaultInvites.downloadInboundInvites()
expect(contactContext.vaultInvites.getCachedPendingInviteRecords()[0].trusted).to.be.false
await Collaboration.createTrustedContactForUserOfContext(contactContext, context)
expect(contactContext.sharedVaults.getCachedPendingInviteRecords()[0].trusted).to.be.true
expect(contactContext.vaultInvites.getCachedPendingInviteRecords()[0].trusted).to.be.true
await deinitContactContext()
})
@@ -184,12 +189,12 @@ describe('shared vault invites', function () {
const { invite, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithUnacceptedButTrustedInvite(context)
const preInvites = await contactContext.sharedVaults.downloadInboundInvites()
const preInvites = await contactContext.vaultInvites.downloadInboundInvites()
expect(preInvites.length).to.equal(1)
await sharedVaults.deleteInvite(invite)
await context.vaultInvites.deleteInvite(invite)
const postInvites = await contactContext.sharedVaults.downloadInboundInvites()
const postInvites = await contactContext.vaultInvites.downloadInboundInvites()
expect(postInvites.length).to.equal(0)
await deinitContactContext()
@@ -208,7 +213,7 @@ describe('shared vault invites', function () {
await contactContext.changePassword('new-password')
await promise
const invites = await contactContext.sharedVaults.downloadInboundInvites()
const invites = await contactContext.vaultInvites.downloadInboundInvites()
expect(invites.length).to.equal(0)
await deinitContactContext()
+5 -4
View File
@@ -8,7 +8,6 @@ describe('shared vault items', function () {
this.timeout(Factory.TwentySecondTimeout)
let context
let sharedVaults
afterEach(async function () {
await context.deinit()
@@ -22,8 +21,6 @@ describe('shared vault items', function () {
await context.launch()
await context.register()
sharedVaults = context.sharedVaults
})
it('should add item to shared vault with no other members', async () => {
@@ -60,7 +57,11 @@ describe('shared vault items', function () {
const currentContextContact = await Collaboration.createTrustedContactForUserOfContext(context, contactContext)
contactContext.lockSyncing()
await sharedVaults.inviteContactToSharedVault(sharedVault, currentContextContact, SharedVaultPermission.Write)
await context.vaultInvites.inviteContactToSharedVault(
sharedVault,
currentContextContact,
SharedVaultPermission.Write,
)
await Collaboration.moveItemToVault(context, sharedVault, note)
const promise = contactContext.awaitNextSyncSharedVaultFromScratchEvent()
+12 -17
View File
@@ -8,8 +8,6 @@ describe('shared vault key rotation', function () {
this.timeout(Factory.TwentySecondTimeout)
let context
let vaults
let sharedVaults
afterEach(async function () {
await context.deinit()
@@ -23,9 +21,6 @@ describe('shared vault key rotation', function () {
await context.launch()
await context.register()
vaults = context.vaults
sharedVaults = context.sharedVaults
})
it('should reencrypt all items keys belonging to key system', async () => {
@@ -37,7 +32,7 @@ describe('shared vault key rotation', function () {
const spy = sinon.spy(context.keys, 'reencryptKeySystemItemsKeysForVault')
const promise = context.resolveWhenSharedVaultKeyRotationInvitesGetSent(sharedVault)
await vaults.rotateVaultRootKey(sharedVault)
await context.vaults.rotateVaultRootKey(sharedVault)
await promise
expect(spy.callCount).to.equal(1)
@@ -52,7 +47,7 @@ describe('shared vault key rotation', function () {
contactContext.lockSyncing()
const promise = context.resolveWhenSharedVaultKeyRotationInvitesGetSent(sharedVault)
await vaults.rotateVaultRootKey(sharedVault)
await context.vaults.rotateVaultRootKey(sharedVault)
await promise
const outboundMessages = await context.asymmetric.getOutboundMessages()
@@ -79,7 +74,7 @@ describe('shared vault key rotation', function () {
contactContext.lockSyncing()
const promise = context.resolveWhenSharedVaultKeyRotationInvitesGetSent(sharedVault)
await vaults.rotateVaultRootKey(sharedVault)
await context.vaults.rotateVaultRootKey(sharedVault)
await promise
const rootKey = context.keys.getPrimaryKeySystemRootKey(sharedVault.systemIdentifier)
@@ -107,7 +102,7 @@ describe('shared vault key rotation', function () {
expect(previousPrimaryItemsKey).to.not.be.undefined
const promise = context.resolveWhenSharedVaultKeyRotationInvitesGetSent(sharedVault)
await vaults.rotateVaultRootKey(sharedVault)
await context.vaults.rotateVaultRootKey(sharedVault)
await promise
const contactPromise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes()
@@ -129,15 +124,15 @@ describe('shared vault key rotation', function () {
await Collaboration.createSharedVaultWithUnacceptedButTrustedInvite(context)
contactContext.lockSyncing()
const originalOutboundInvites = await sharedVaults.getOutboundInvites()
const originalOutboundInvites = await context.vaultInvites.getOutboundInvites()
expect(originalOutboundInvites.length).to.equal(1)
const originalInviteMessage = originalOutboundInvites[0].encrypted_message
const promise = context.resolveWhenSharedVaultKeyRotationInvitesGetSent(sharedVault)
await vaults.rotateVaultRootKey(sharedVault)
await context.vaults.rotateVaultRootKey(sharedVault)
await promise
const updatedOutboundInvites = await sharedVaults.getOutboundInvites()
const updatedOutboundInvites = await context.vaultInvites.getOutboundInvites()
expect(updatedOutboundInvites.length).to.equal(1)
const joinInvite = updatedOutboundInvites[0]
@@ -150,7 +145,7 @@ describe('shared vault key rotation', function () {
it('new key system items key in rotated shared vault should belong to shared vault', async () => {
const sharedVault = await Collaboration.createSharedVault(context)
await vaults.rotateVaultRootKey(sharedVault)
await context.vaults.rotateVaultRootKey(sharedVault)
const keySystemItemsKeys = context.keys
.getAllKeySystemItemsKeys()
@@ -170,7 +165,7 @@ describe('shared vault key rotation', function () {
contactContext.lockSyncing()
const firstPromise = context.resolveWhenSharedVaultKeyRotationInvitesGetSent(sharedVault)
await vaults.rotateVaultRootKey(sharedVault)
await context.vaults.rotateVaultRootKey(sharedVault)
await firstPromise
const asymmetricMessageAfterFirstChange = await context.asymmetric.getOutboundMessages()
@@ -180,7 +175,7 @@ describe('shared vault key rotation', function () {
const messageAfterFirstChange = asymmetricMessageAfterFirstChange[0]
const secondPromise = context.resolveWhenSharedVaultKeyRotationInvitesGetSent(sharedVault)
await vaults.rotateVaultRootKey(sharedVault)
await context.vaults.rotateVaultRootKey(sharedVault)
await secondPromise
const asymmetricMessageAfterSecondChange = await context.asymmetric.getOutboundMessages()
@@ -204,7 +199,7 @@ describe('shared vault key rotation', function () {
contactContext.lockSyncing()
const promise = context.resolveWhenSharedVaultKeyRotationInvitesGetSent(sharedVault)
await vaults.rotateVaultRootKey(sharedVault)
await context.vaults.rotateVaultRootKey(sharedVault)
await promise
const acceptMessage = sinon.spy(contactContext.asymmetric, 'handleTrustedSharedVaultRootKeyChangedMessage')
@@ -223,7 +218,7 @@ describe('shared vault key rotation', function () {
const originalKeySystemRootKey = context.keys.getPrimaryKeySystemRootKey(sharedVault.systemIdentifier)
await sharedVaults.removeUserFromSharedVault(sharedVault, contactContext.userUuid)
await context.vaultUsers.removeUserFromSharedVault(sharedVault, contactContext.userUuid)
const newKeySystemRootKey = context.keys.getPrimaryKeySystemRootKey(sharedVault.systemIdentifier)
@@ -8,7 +8,6 @@ describe('shared vault permissions', function () {
this.timeout(Factory.TwentySecondTimeout)
let context
let sharedVaults
afterEach(async function () {
await context.deinit()
@@ -22,8 +21,6 @@ describe('shared vault permissions', function () {
await context.launch()
await context.register()
sharedVaults = context.sharedVaults
})
it('non-admin user should not be able to invite user', async () => {
@@ -37,7 +34,7 @@ describe('shared vault permissions', function () {
contactContext,
thirdParty.contactContext,
)
const result = await contactContext.sharedVaults.inviteContactToSharedVault(
const result = await contactContext.vaultInvites.inviteContactToSharedVault(
sharedVault,
thirdPartyContact,
SharedVaultPermission.Write,
@@ -53,16 +50,15 @@ describe('shared vault permissions', function () {
const sharedVault = await Collaboration.createSharedVault(context)
const result = await sharedVaults.removeUserFromSharedVault(sharedVault, context.userUuid)
expect(isClientDisplayableError(result)).to.be.true
const result = await context.vaultUsers.removeUserFromSharedVault(sharedVault, context.userUuid)
expect(result.isFailed()).to.be.true
})
it('should be able to leave shared vault as added admin', async () => {
const { contactVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context, SharedVaultPermission.Admin)
const result = await contactContext.sharedVaults.leaveSharedVault(contactVault)
const result = await contactContext.vaultUsers.leaveSharedVault(contactVault)
expect(isClientDisplayableError(result)).to.be.false
@@ -54,9 +54,8 @@ describe('shared vaults', function () {
const { sharedVault, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInvite(context)
const result = await context.sharedVaults.removeUserFromSharedVault(sharedVault, contactContext.userUuid)
expect(result).to.be.undefined
const result = await context.vaultUsers.removeUserFromSharedVault(sharedVault, contactContext.userUuid)
expect(result.isFailed()).to.be.false
const promise = contactContext.resolveWhenUserMessagesProcessingCompletes()
await contactContext.sync()
+99 -4
View File
@@ -23,11 +23,106 @@ describe('signatures', function () {
await context.register()
})
it('signatures should be marked as of questionable integrity when signed with non root contact public key', async () => {
console.error('TODO: implement test')
describe('item decryption signature verification', () => {
it('should have failing signature if contact public key does not match', async () => {
const { note, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
const regularContact = contactContext.contacts.findContact(context.userUuid)
const decoyContact = new TrustedContact(
regularContact.payload.copy({
content: {
...regularContact.payload.content,
publicKeySet: ContactPublicKeySet.FromJson({
...regularContact.payload.content.publicKeySet,
encryption: 'invalid public key',
signing: 'invalid signing public key',
}),
},
}),
)
contactContext.items.collection.onChange({
changed: [decoyContact],
inserted: [],
discarded: [],
ignored: [],
unerrored: [],
})
await context.changeNoteTitle(note, 'new title')
await contactContext.sync()
const contactNote = contactContext.items.findItem(note.uuid)
/** Signature data only verifies whether the embedded signature and embedded signature public key match up */
expect(contactNote.signatureData.required).to.be.true
expect(contactNote.signatureData.result.passes).to.be.true
const status = contactContext.contacts.getItemSignatureStatus(contactNote)
expect(status).to.equal(ItemSignatureValidationResult.NotTrusted)
await deinitContactContext()
})
})
it('items marked with questionable integrity should have option to trust the item which would resync it', async () => {
console.error('TODO: implement test')
describe('UI signature status check', () => {
it('signatures should be trusted with root public key', async () => {
const { note, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
const contactNote = contactContext.items.findItem(note.uuid)
const status = contactContext.contacts.getItemSignatureStatus(contactNote)
expect(status).to.equal(ItemSignatureValidationResult.Trusted)
await deinitContactContext()
})
it('signatures return SignedWithNonCurrentKey when signed with non root contact public key', async () => {
const { note, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
await context.changePassword('new password')
await contactContext.sync()
const contactNote = contactContext.items.findItem(note.uuid)
const status = contactContext.contacts.getItemSignatureStatus(contactNote)
expect(status).to.equal(ItemSignatureValidationResult.SignedWithNonCurrentKey)
await deinitContactContext()
})
it('syncing a SignedWithNonCurrentKey item should reset its status', async () => {
const { note, contactContext, deinitContactContext } =
await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context)
await context.changePassword('new password')
await contactContext.sync()
const contactNote = contactContext.items.findItem(note.uuid)
const latestNote = await contactContext.changeNoteTitle(contactNote, 'new title')
const status = contactContext.contacts.getItemSignatureStatus(latestNote)
expect(status).to.equal(ItemSignatureValidationResult.NotApplicable)
await deinitContactContext()
})
it('should return NotApplicable if item does not belong to shared vault', async () => {
const item = await context.createSyncedNote()
const status = context.contacts.getItemSignatureStatus(item)
expect(status).to.equal(ItemSignatureValidationResult.NotApplicable)
})
})
})
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/snjs",
"version": "2.202.10",
"version": "2.202.12",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.28.10](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-24)
**Note:** Version bump only for package @standardnotes/ui-services
## [1.28.9](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-24)
**Note:** Version bump only for package @standardnotes/ui-services
## [1.28.8](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-23)
**Note:** Version bump only for package @standardnotes/ui-services
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/ui-services",
"version": "1.28.8",
"version": "1.28.10",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.167.9](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-24)
**Note:** Version bump only for package @standardnotes/web
## [3.167.8](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-24)
**Note:** Version bump only for package @standardnotes/web
## [3.167.7](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-24)
**Note:** Version bump only for package @standardnotes/web
+22
View File
@@ -1,5 +1,27 @@
{
"versions": [
{
"version": "3.167.9",
"title": "[3.167.9](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-24)",
"date": null,
"body": "**Note:** Version bump only for package @standardnotes/web",
"parsed": {
"_": [
"Note: Version bump only for package @standardnotes/web"
]
}
},
{
"version": "3.167.8",
"title": "[3.167.8](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-24)",
"date": null,
"body": "**Note:** Version bump only for package @standardnotes/web",
"parsed": {
"_": [
"Note: Version bump only for package @standardnotes/web"
]
}
},
{
"version": "3.167.7",
"title": "[3.167.7](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-24)",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/web",
"version": "3.167.7",
"version": "3.167.9",
"license": "AGPL-3.0-or-later",
"main": "dist/app.js",
"author": "Standard Notes.",
@@ -61,14 +61,14 @@ export const PreprocessingStep = ({
useEffect(() => {
const processPendingInvites = async () => {
await application.sharedVaults.downloadInboundInvites()
const hasPendingInvites = application.sharedVaults.getCachedPendingInviteRecords().length > 0
await application.vaultInvites.downloadInboundInvites()
const hasPendingInvites = application.vaultInvites.getCachedPendingInviteRecords().length > 0
setNeedsUserConfirmation(hasPendingInvites ? 'yes' : 'no')
setIsProcessingInvites(false)
}
void processPendingInvites()
}, [application.sharedVaults])
}, [application])
const isProcessing = isProcessingSync || isProcessingMessages || isProcessingInvites
@@ -2,10 +2,10 @@ import { FunctionComponent, useCallback, useEffect, useMemo, useState } from 're
import Modal, { ModalAction } from '@/Components/Modal/Modal'
import DecoratedInput from '@/Components/Input/DecoratedInput'
import { useApplication } from '@/Components/ApplicationProvider'
import { PendingSharedVaultInviteRecord, TrustedContactInterface } from '@standardnotes/snjs'
import { InviteRecord, TrustedContactInterface } from '@standardnotes/snjs'
type Props = {
fromInvite?: PendingSharedVaultInviteRecord
fromInvite?: InviteRecord
editContactUuid?: string
onCloseDialog: () => void
onAddContact?: (contact: TrustedContactInterface) => void
@@ -30,7 +30,7 @@ const EditContactModal: FunctionComponent<Props> = ({ onCloseDialog, fromInvite,
useEffect(() => {
if (editContactUuid) {
const contact = application.contacts.findTrustedContact(editContactUuid)
const contact = application.contacts.findContact(editContactUuid)
if (!contact) {
throw new Error(`Contact with uuid ${editContactUuid} not found`)
}
@@ -16,11 +16,11 @@ const ContactInviteModal: FunctionComponent<Props> = ({ vault, onCloseDialog })
useEffect(() => {
const loadContacts = async () => {
const contacts = await application.sharedVaults.getInvitableContactsForSharedVault(vault)
const contacts = await application.vaultInvites.getInvitableContactsForSharedVault(vault)
setContacts(contacts)
}
void loadContacts()
}, [application.sharedVaults, vault])
}, [application.vaultInvites, vault])
const handleDialogClose = useCallback(() => {
onCloseDialog()
@@ -28,10 +28,10 @@ const ContactInviteModal: FunctionComponent<Props> = ({ vault, onCloseDialog })
const inviteSelectedContacts = useCallback(async () => {
for (const contact of selectedContacts) {
await application.sharedVaults.inviteContactToSharedVault(vault, contact, SharedVaultPermission.Write)
await application.vaultInvites.inviteContactToSharedVault(vault, contact, SharedVaultPermission.Write)
}
handleDialogClose()
}, [application.sharedVaults, vault, handleDialogClose, selectedContacts])
}, [application.vaultInvites, vault, handleDialogClose, selectedContacts])
const toggleContact = useCallback(
(contact: TrustedContactInterface) => {
@@ -2,13 +2,13 @@ import { useApplication } from '@/Components/ApplicationProvider'
import Button from '@/Components/Button/Button'
import Icon from '@/Components/Icon/Icon'
import ModalOverlay from '@/Components/Modal/ModalOverlay'
import { PendingSharedVaultInviteRecord } from '@standardnotes/snjs'
import { InviteRecord } from '@standardnotes/snjs'
import { useCallback, useState } from 'react'
import EditContactModal from '../Contacts/EditContactModal'
import { CheckmarkCircle } from '../../../../UIElements/CheckmarkCircle'
type Props = {
inviteRecord: PendingSharedVaultInviteRecord
inviteRecord: InviteRecord
}
const InviteItem = ({ inviteRecord }: Props) => {
@@ -23,13 +23,13 @@ const InviteItem = ({ inviteRecord }: Props) => {
}, [])
const acceptInvite = useCallback(async () => {
await application.sharedVaults.acceptPendingSharedVaultInvite(inviteRecord)
}, [application.sharedVaults, inviteRecord])
await application.vaultInvites.acceptInvite(inviteRecord)
}, [application, inviteRecord])
const closeAddContactModal = () => setIsAddContactModalOpen(false)
const collaborationId = application.contacts.getCollaborationIDFromInvite(inviteRecord.invite)
const trustedContact = application.contacts.findTrustedContactForInvite(inviteRecord.invite)
const trustedContact = application.contacts.findContactForInvite(inviteRecord.invite)
return (
<>
@@ -10,9 +10,10 @@ import { useCallback, useEffect, useState } from 'react'
import {
VaultListingInterface,
TrustedContactInterface,
PendingSharedVaultInviteRecord,
InviteRecord,
ContentType,
SharedVaultServiceEvent,
VaultUserServiceEvent,
} from '@standardnotes/snjs'
import VaultItem from './Vaults/VaultItem'
import Button from '@/Components/Button/Button'
@@ -23,7 +24,7 @@ const Vaults = () => {
const application = useApplication()
const [vaults, setVaults] = useState<VaultListingInterface[]>([])
const [invites, setInvites] = useState<PendingSharedVaultInviteRecord[]>([])
const [invites, setInvites] = useState<InviteRecord[]>([])
const [contacts, setContacts] = useState<TrustedContactInterface[]>([])
const [isAddContactModalOpen, setIsAddContactModalOpen] = useState(false)
@@ -33,7 +34,6 @@ const Vaults = () => {
const closeVaultModal = () => setIsVaultModalOpen(false)
const vaultService = application.vaults
const sharedVaultService = application.sharedVaults
const contactService = application.contacts
const updateVaults = useCallback(async () => {
@@ -41,8 +41,8 @@ const Vaults = () => {
}, [vaultService])
const updateInvites = useCallback(async () => {
setInvites(sharedVaultService.getCachedPendingInviteRecords())
}, [sharedVaultService])
setInvites(application.vaultInvites.getCachedPendingInviteRecords())
}, [application.vaultInvites])
const updateContacts = useCallback(async () => {
setContacts(contactService.getAllContacts())
@@ -60,6 +60,20 @@ const Vaults = () => {
})
}, [application.sharedVaults, updateAllData])
useEffect(() => {
return application.vaultUsers.addEventObserver((event) => {
if (event === VaultUserServiceEvent.UsersChanged) {
void updateAllData()
}
})
}, [application.vaultUsers, updateAllData])
useEffect(() => {
return application.vaultInvites.addEventObserver(() => {
void updateAllData()
})
}, [application.vaultInvites, updateAllData])
useEffect(() => {
return application.streamItems([ContentType.TYPES.VaultListing, ContentType.TYPES.TrustedContact], () => {
void updateAllData()
@@ -67,9 +81,9 @@ const Vaults = () => {
}, [application, updateAllData])
useEffect(() => {
void sharedVaultService.downloadInboundInvites()
void application.vaultInvites.downloadInboundInvites()
void updateAllData()
}, [updateAllData, sharedVaultService])
}, [updateAllData, application.vaultInvites])
const createNewVault = useCallback(async () => {
setIsVaultModalOpen(true)
@@ -20,7 +20,7 @@ const VaultItem = ({ vault }: Props) => {
const [isVaultModalOpen, setIsVaultModalOpen] = useState(false)
const closeVaultModal = () => setIsVaultModalOpen(false)
const isAdmin = !vault.isSharedVaultListing() ? true : application.sharedVaults.isCurrentUserSharedVaultAdmin(vault)
const isAdmin = !vault.isSharedVaultListing() ? true : application.vaultUsers.isCurrentUserSharedVaultAdmin(vault)
const deleteVault = useCallback(async () => {
const confirm = await application.alerts.confirm(
@@ -61,11 +61,11 @@ const VaultItem = ({ vault }: Props) => {
return
}
const success = await application.sharedVaults.leaveSharedVault(vault)
const success = await application.vaultUsers.leaveSharedVault(vault)
if (!success) {
void application.alerts.alert('Unable to leave vault. Please try again.')
}
}, [application.alerts, application.sharedVaults, vault])
}, [application, vault])
const convertToSharedVault = useCallback(async () => {
await application.sharedVaults.convertVaultToSharedVault(vault)
@@ -54,20 +54,20 @@ const EditVaultModal: FunctionComponent<Props> = ({ onCloseDialog, existingVault
if (existingVault.isSharedVaultListing()) {
setIsAdmin(
existingVault.isSharedVaultListing() && application.sharedVaults.isCurrentUserSharedVaultAdmin(existingVault),
existingVault.isSharedVaultListing() && application.vaultUsers.isCurrentUserSharedVaultAdmin(existingVault),
)
const users = await application.sharedVaults.getSharedVaultUsers(existingVault)
const users = await application.vaultUsers.getSharedVaultUsers(existingVault)
if (users) {
setMembers(users)
}
const invites = await application.sharedVaults.getOutboundInvites(existingVault)
const invites = await application.vaultInvites.getOutboundInvites(existingVault)
if (!isClientDisplayableError(invites)) {
setInvites(invites)
}
}
}, [application.sharedVaults, existingVault])
}, [application, existingVault])
useEffect(() => {
void reloadVaultInfo()
@@ -17,17 +17,17 @@ export const VaultModalInvites = ({
const deleteInvite = useCallback(
async (invite: SharedVaultInviteServerHash) => {
await application.sharedVaults.deleteInvite(invite)
await application.vaultInvites.deleteInvite(invite)
onChange()
},
[application.sharedVaults, onChange],
[application.vaultInvites, onChange],
)
return (
<div className="mb-3">
<div className="mb-3 text-lg">Pending Invites</div>
{invites.map((invite) => {
const contact = application.contacts.findTrustedContactForInvite(invite)
const contact = application.contacts.findContactForInvite(invite)
return (
<div key={invite.uuid} className="bg-gray-100 flex flex-row gap-3.5 rounded-lg px-3.5 py-2.5 shadow-md">
<Icon type={'user'} size="custom" className="mt-2.5 h-5.5 w-5.5 flex-shrink-0" />
@@ -20,22 +20,22 @@ export const VaultModalMembers = ({
const removeMemberFromVault = useCallback(
async (memberItem: SharedVaultUserServerHash) => {
if (vault.isSharedVaultListing()) {
await application.sharedVaults.removeUserFromSharedVault(vault, memberItem.user_uuid)
await application.vaultUsers.removeUserFromSharedVault(vault, memberItem.user_uuid)
onChange()
}
},
[application.sharedVaults, vault, onChange],
[application.vaultUsers, vault, onChange],
)
return (
<div className="mb-3">
<div className="mb-3 text-lg">Vault Members</div>
{members.map((member) => {
if (application.sharedVaults.isSharedVaultUserSharedVaultOwner(member)) {
if (application.vaultUsers.isVaultUserOwner(member)) {
return null
}
const contact = application.contacts.findTrustedContactForServerUser(member)
const contact = application.contacts.findContactForServerUser(member)
return (
<div
key={contact?.uuid || member.user_uuid}