Compare commits

...
97 changed files with 1193 additions and 849 deletions
-3
View File
@@ -6,6 +6,3 @@ DEFAULT_SYNC_SERVER=https://api.standardnotes.com
DASHBOARD_URL=http://standardnotes.com/dashboard
PLANS_URL=https://standardnotes.com/plans
PURCHASE_URL=https://standardnotes.com/purchase
# Used by Rails internals and not Standard Notes related
SECRET_KEY_BASE=<enter a secret string here>
Binary file not shown.
Binary file not shown.
+12
View File
@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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.108](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.107](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.107",
"version": "1.1.110",
"private": true,
"scripts": {
"build-mv2": "yarn clean && webpack --config ./webpack.config.prod.js",
+12
View File
@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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
## [3.108.39](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.38](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.38",
"version": "3.108.41",
"license": "AGPL-3.0-or-later",
"author": "Standard Notes.",
"private": true,
+12
View File
@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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
## [3.56.18](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.17](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-24)
**Note:** Version bump only for package @standardnotes/mobile
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/mobile",
"version": "3.56.17",
"version": "3.56.20",
"author": "Standard Notes.",
"private": true,
"license": "AGPL-3.0-or-later",
+12
View File
@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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.4.390](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.389](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.389",
"version": "1.4.392",
"license": "AGPL-3.0-or-later",
"main": "dist/releases.json",
"types": "dist/index.d.ts",
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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.7",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
@@ -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,24 +1,15 @@
import { IsVaultAdmin } from './../VaultUser/UseCase/IsVaultAdmin'
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'
@@ -40,56 +31,37 @@ 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 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<IsVaultAdmin>
const eventBus = {} as jest.Mocked<InternalEventBusInterface>
eventBus.addEventHandler = jest.fn()
service = new SharedVaultService(
sync,
items,
encryption,
session,
vaults,
invitesServer,
getVault,
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 { IsVaultAdmin } from '../VaultUser/UseCase/IsVaultAdmin'
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 _createSharedVaultUseCase: CreateSharedVault,
private _handleKeyPairChange: HandleKeyPairChange,
private _notifyVaultUsersOfKeyRotation: NotifyVaultUsersOfKeyRotation,
private _sendVaultDataChangeMessage: SendVaultDataChangedMessage,
private _findContact: FindContact,
private _deleteThirdPartyVault: DeleteThirdPartyVault,
private _shareContactWithVault: ShareContactWithVault,
private _convertToSharedVault: ConvertToSharedVault,
private _deleteSharedVaultUseCase: DeleteSharedVault,
private _isVaultAdmin: IsVaultAdmin,
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,88 @@ export class SharedVaultService
)
}
override deinit(): void {
super.deinit()
;(this.items as unknown) = undefined
;(this.encryption as unknown) = undefined
;(this.session as unknown) = undefined
;(this.vaults 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._findContact as unknown) = undefined
;(this._deleteThirdPartyVault as unknown) = undefined
;(this._shareContactWithVault as unknown) = undefined
;(this._convertToSharedVault as unknown) = undefined
;(this._deleteSharedVaultUseCase 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 +173,7 @@ export class SharedVaultService
userInputtedPassword: string | undefined
storagePreference?: KeySystemRootKeyStorageMode
}): Promise<VaultListingInterface | ClientDisplayableError> {
return this.createSharedVaultUseCase.execute({
return this._createSharedVaultUseCase.execute({
vaultName: dto.name,
vaultDescription: dto.description,
userInputtedPassword: dto.userInputtedPassword,
@@ -194,11 +184,7 @@ export class SharedVaultService
async convertVaultToSharedVault(
vault: VaultListingInterface,
): Promise<SharedVaultListingInterface | ClientDisplayableError> {
return this.convertToSharedVault.execute({ vault })
}
public getCachedPendingInviteRecords(): PendingSharedVaultInviteRecord[] {
return Object.values(this.pendingInvites)
return this._convertToSharedVault.execute({ vault })
}
private getAllSharedVaults(): SharedVaultListingInterface[] {
@@ -206,38 +192,6 @@ export class SharedVaultService
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()
}
private async handleTrustedContactsChange(contacts: TrustedContactInterface[]): Promise<void> {
for (const contact of contacts) {
if (contact.isMe) {
@@ -254,7 +208,7 @@ export class SharedVaultService
continue
}
await this.sendVaultDataChangeMessage.execute({
await this._sendVaultDataChangeMessage.execute({
vault,
senderUuid: this.session.getSureUser().uuid,
keys: {
@@ -265,220 +219,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._deleteSharedVaultUseCase.execute({ sharedVault })
}
async shareContactWithVaults(contact: TrustedContactInterface): Promise<void> {
@@ -486,10 +228,17 @@ export class SharedVaultService
throw new Error('Cannot share self contact')
}
const ownedVaults = this.getAllSharedVaults().filter(this.isCurrentUserSharedVaultAdmin.bind(this))
const ownedVaults = this.getAllSharedVaults().filter((vault) => {
return this._isVaultAdmin
.execute({
sharedVault: vault,
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 +255,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 +265,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>
}
@@ -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'
@@ -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'
@@ -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'
@@ -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(
@@ -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,274 @@
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(sharedVault.sharing.sharedVaultUuid)
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,
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>
}
@@ -0,0 +1,12 @@
import { Result, SyncUseCaseInterface } from '@standardnotes/domain-core'
import { SharedVaultListingInterface } from '@standardnotes/models'
export class IsVaultAdmin 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,103 @@
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 { IsVaultAdmin } from './UseCase/IsVaultAdmin'
export class VaultUserService extends AbstractService<VaultUserServiceEvent> implements VaultUserServiceInterface {
constructor(
private session: SessionsClientInterface,
private vaults: VaultServiceInterface,
private _getVaultUsers: GetVaultUsers,
private _removeVaultMember: RemoveVaultMember,
private _isVaultAdmin: IsVaultAdmin,
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._isVaultAdmin as unknown) = undefined
;(this._getVault as unknown) = undefined
;(this._leaveVault as unknown) = undefined
}
public async getSharedVaultUsers(
sharedVault: SharedVaultListingInterface,
): Promise<SharedVaultUserServerHash[] | undefined> {
return this._getVaultUsers.execute({ sharedVaultUuid: sharedVault.sharing.sharedVaultUuid })
}
public isCurrentUserSharedVaultAdmin(sharedVault: SharedVaultListingInterface): boolean {
return this._isVaultAdmin
.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
}
+23 -17
View File
@@ -57,15 +57,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 +111,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 +122,25 @@ 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/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/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,6 +173,16 @@ 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'
@@ -197,3 +194,12 @@ 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/IsVaultAdmin'
export * from './VaultUser/UseCase/LeaveSharedVault'
export * from './VaultUser/UseCase/RemoveSharedVaultMember'
export * from './VaultUser/VaultUserService'
export * from './VaultUser/VaultUserServiceEvent'
export * from './VaultUser/VaultUserServiceInterface'
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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,9 @@ import {
RootKeyManager,
ItemsEncryptionService,
DecryptBackupFile,
VaultUserService,
IsVaultAdmin,
VaultInviteService,
} from '@standardnotes/services'
import { ItemManager } from '../../Services/Items/ItemManager'
import { PayloadManager } from '../../Services/Payloads/PayloadManager'
@@ -204,6 +207,10 @@ export class Dependencies {
)
})
this.factory.set(TYPES.IsVaultAdmin, () => {
return new IsVaultAdmin()
})
this.factory.set(TYPES.DecryptBackupFile, () => {
return new DecryptBackupFile(this.get(TYPES.EncryptionService))
})
@@ -608,6 +615,39 @@ 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.IsVaultAdmin),
this.get(TYPES.GetVault),
this.get(TYPES.LeaveVault),
this.get(TYPES.InternalEventBus),
)
})
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),
@@ -630,31 +670,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.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.IsVaultAdmin),
this.get(TYPES.InternalEventBus),
)
})
@@ -59,6 +59,8 @@ export const TYPES = {
EncryptionOperators: Symbol.for('EncryptionOperators'),
RootKeyManager: Symbol.for('RootKeyManager'),
ItemsEncryptionService: Symbol.for('ItemsEncryptionService'),
VaultUserService: Symbol.for('VaultUserService'),
VaultInviteService: Symbol.for('VaultInviteService'),
// Servers
RevisionServer: Symbol.for('RevisionServer'),
@@ -140,6 +142,7 @@ export const TYPES = {
EncryptTypeAPayload: Symbol.for('EncryptTypeAPayload'),
EncryptTypeAPayloadWithKeyLookup: Symbol.for('EncryptTypeAPayloadWithKeyLookup'),
DecryptBackupFile: Symbol.for('DecryptBackupFile'),
IsVaultAdmin: Symbol.for('IsVaultAdmin'),
// Mappers
SessionStorageMapper: Symbol.for('SessionStorageMapper'),
+9 -11
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)
+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()
+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
+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()
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/snjs",
"version": "2.202.10",
"version": "2.202.11",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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.9",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+12
View File
@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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
## [3.167.6](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.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-24)
**Note:** Version bump only for package @standardnotes/web
+33
View File
@@ -1,5 +1,38 @@
{
"versions": [
{
"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)",
"date": null,
"body": "**Note:** Version bump only for package @standardnotes/web",
"parsed": {
"_": [
"Note: Version bump only for package @standardnotes/web"
]
}
},
{
"version": "3.167.6",
"title": "[3.167.6](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.5",
"title": "[3.167.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-07-24)",
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/web",
"version": "3.167.5",
"version": "3.167.8",
"license": "AGPL-3.0-or-later",
"main": "dist/app.js",
"author": "Standard Notes.",
@@ -15,7 +15,7 @@
"lint": "eslint src/javascripts && yarn tsc",
"lint:fix": "eslint src/javascripts --fix",
"start": "webpack-dev-server --config web.webpack.dev.js",
"start-secure": "yarn start --server-type https",
"start-secure": "yarn start --https",
"test": "jest --config jest.config.js",
"tsc": "tsc --project tsconfig.json",
"upgrade:snjs": "ncu -u '@standardnotes/*'",
@@ -28,7 +28,7 @@
"@babel/plugin-transform-react-jsx": "^7.19.0",
"@babel/preset-env": "*",
"@babel/preset-typescript": "^7.21.5",
"@lexical/react": "0.11.2",
"@lexical/react": "0.11.3",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.10",
"@simplewebauthn/browser": "^7.1.0",
"@standardnotes/authenticator": "^2.4.0",
@@ -76,7 +76,7 @@
"identity-obj-proxy": "^3.0.0",
"jest": "^29.3.1",
"jest-environment-jsdom": "^29.3.1",
"lexical": "0.11.2",
"lexical": "0.11.3",
"lint-staged": ">=13",
"mini-css-extract-plugin": "^2.7.2",
"minimatch": "^5.1.1",
@@ -109,7 +109,7 @@
},
"dependencies": {
"@ariakit/react": "^0.2.8",
"@lexical/headless": "0.11.2",
"@lexical/headless": "0.11.3",
"@radix-ui/react-slot": "^1.0.1",
"fast-diff": "^1.3.0"
}
@@ -7,7 +7,7 @@ const IosKeyboardClose = () => {
const application = useApplication()
const [isVisible, setIsVisible] = useState(false)
const [isFocusInSuperEditor, setIsFocusInSuperEditor] = useState(
() => !!document.activeElement?.closest('#blocks-editor'),
() => !!document.activeElement?.closest('#blocks-editor,#super-mobile-toolbar'),
)
useEffect(() => {
@@ -22,7 +22,7 @@ const IosKeyboardClose = () => {
useEffect(() => {
const handleFocusChange = () => {
setIsFocusInSuperEditor(!!document.activeElement?.closest('#blocks-editor'))
setIsFocusInSuperEditor(!!document.activeElement?.closest('#blocks-editor,#super-mobile-toolbar'))
}
document.addEventListener('focusin', handleFocusChange)
@@ -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
@@ -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,8 +23,8 @@ 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)
@@ -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,10 +17,10 @@ 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 (
@@ -20,18 +20,18 @@ 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
}
@@ -20,7 +20,6 @@ import CollapsiblePlugin from './Plugins/CollapsiblePlugin'
import DraggableBlockPlugin from './Plugins/DraggableBlockPlugin'
import CodeHighlightPlugin from './Plugins/CodeHighlightPlugin'
import FloatingTextFormatToolbarPlugin from './Plugins/FloatingTextFormatToolbarPlugin'
import FloatingLinkEditorPlugin from './Plugins/FloatingLinkEditorPlugin'
import { TabIndentationPlugin } from './Plugins/TabIndentationPlugin'
import { handleEditorChange } from './Utils'
import { SuperEditorContentId } from './Constants'
@@ -109,7 +108,6 @@ export const BlocksEditor: FunctionComponent<BlocksEditorProps> = ({
{!readonly && floatingAnchorElem && (
<>
<FloatingTextFormatToolbarPlugin anchorElem={floatingAnchorElem} />
<FloatingLinkEditorPlugin anchorElem={floatingAnchorElem} />
<DraggableBlockPlugin anchorElem={floatingAnchorElem} />
</>
)}
@@ -4,7 +4,7 @@ import { KeyboardKey } from '@standardnotes/ui-services'
import { IconComponent } from '../../Lexical/Theme/IconComponent'
import { sanitizeUrl } from '../../Lexical/Utils/sanitizeUrl'
import { TOGGLE_LINK_COMMAND } from '@lexical/link'
import { useCallback, useState } from 'react'
import { useCallback, useState, useRef } from 'react'
import { GridSelection, LexicalEditor, NodeSelection, RangeSelection } from 'lexical'
import { classNames } from '@standardnotes/snjs'
@@ -19,6 +19,7 @@ type Props = {
const LinkEditor = ({ linkUrl, isEditMode, setEditMode, editor, lastSelection, isAutoLink }: Props) => {
const [editedLinkUrl, setEditedLinkUrl] = useState('')
const editModeContainer = useRef<HTMLDivElement>(null)
const handleLinkSubmission = () => {
if (lastSelection !== null) {
@@ -36,7 +37,7 @@ const LinkEditor = ({ linkUrl, isEditMode, setEditMode, editor, lastSelection, i
}, [])
return isEditMode ? (
<div className="flex items-center gap-2">
<div className="flex items-center gap-2" ref={editModeContainer}>
<input
id="link-input"
ref={focusInput}
@@ -53,6 +54,11 @@ const LinkEditor = ({ linkUrl, isEditMode, setEditMode, editor, lastSelection, i
setEditMode(false)
}
}}
onBlur={(event) => {
if (!editModeContainer.current?.contains(event.relatedTarget as Node)) {
setEditMode(false)
}
}}
className="flex-grow rounded-sm bg-contrast p-1 text-text sm:min-w-[40ch]"
/>
<button
@@ -11,11 +11,15 @@ import {
CAN_UNDO_COMMAND,
COMMAND_PRIORITY_CRITICAL,
FORMAT_TEXT_COMMAND,
GridSelection,
NodeSelection,
REDO_COMMAND,
RangeSelection,
SELECTION_CHANGE_COMMAND,
UNDO_COMMAND,
} from 'lexical'
import { mergeRegister } from '@lexical/utils'
import { $isLinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link'
import { $isLinkNode, $isAutoLinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { GetAlignmentBlocks } from '../Blocks/Alignment'
import { GetBulletedListBlock } from '../Blocks/BulletedList'
@@ -38,6 +42,7 @@ import { SUPER_TOGGLE_SEARCH } from '@standardnotes/ui-services'
import { useApplication } from '@/Components/ApplicationProvider'
import { GetRemoteImageBlock } from '../Blocks/RemoteImage'
import { InsertRemoteImageDialog } from '../RemoteImagePlugin/RemoteImagePlugin'
import LinkEditor from '../FloatingLinkEditorPlugin/LinkEditor'
const MobileToolbarPlugin = () => {
const application = useApplication()
@@ -45,10 +50,12 @@ const MobileToolbarPlugin = () => {
const [modal, showModal] = useModal()
const [isInEditor, setIsInEditor] = useState(false)
const [isInLinkEditor, setIsInLinkEditor] = useState(false)
const [isInToolbar, setIsInToolbar] = useState(false)
const isMobile = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.sm)
const toolbarRef = useRef<HTMLDivElement>(null)
const linkEditorRef = useRef<HTMLDivElement>(null)
const backspaceButtonRef = useRef<HTMLButtonElement>(null)
const insertLink = useCallback(() => {
@@ -214,8 +221,10 @@ const MobileToolbarPlugin = () => {
const handleBlur = (event: FocusEvent) => {
const elementToBeFocused = event.relatedTarget as Node
const toolbarContainsElementToFocus = toolbarRef.current && toolbarRef.current.contains(elementToBeFocused)
const linkEditorContainsElementToFocus =
linkEditorRef.current && linkEditorRef.current.contains(elementToBeFocused)
const willFocusBackspaceButton = backspaceButtonRef.current && elementToBeFocused === backspaceButtonRef.current
if (toolbarContainsElementToFocus || willFocusBackspaceButton) {
if (toolbarContainsElementToFocus || linkEditorContainsElementToFocus || willFocusBackspaceButton) {
return
}
setIsInEditor(false)
@@ -231,31 +240,116 @@ const MobileToolbarPlugin = () => {
}, [editor])
useEffect(() => {
if (!toolbarRef.current) {
return
}
const toolbar = toolbarRef.current
const linkEditor = linkEditorRef.current
const handleFocus = () => setIsInToolbar(true)
const handleBlur = (event: FocusEvent) => {
const handleToolbarFocus = () => setIsInToolbar(true)
const handleLinkEditorFocus = () => setIsInLinkEditor(true)
const handleToolbarBlur = (event: FocusEvent) => {
const elementToBeFocused = event.relatedTarget as Node
if (elementToBeFocused === backspaceButtonRef.current) {
return
}
setIsInToolbar(false)
}
const handleLinkEditorBlur = (event: FocusEvent) => {
const elementToBeFocused = event.relatedTarget as Node
if (elementToBeFocused === backspaceButtonRef.current) {
return
}
setIsInLinkEditor(false)
}
toolbar.addEventListener('focus', handleFocus)
toolbar.addEventListener('blur', handleBlur)
if (toolbar) {
toolbar.addEventListener('focus', handleToolbarFocus)
toolbar.addEventListener('blur', handleToolbarBlur)
}
if (linkEditor) {
linkEditor.addEventListener('focus', handleLinkEditorFocus)
linkEditor.addEventListener('blur', handleLinkEditorBlur)
}
return () => {
toolbar?.removeEventListener('focus', handleFocus)
toolbar?.removeEventListener('blur', handleBlur)
toolbar?.removeEventListener('focus', handleToolbarFocus)
toolbar?.removeEventListener('blur', handleToolbarBlur)
linkEditor?.removeEventListener('focus', handleLinkEditorFocus)
linkEditor?.removeEventListener('blur', handleLinkEditorBlur)
}
}, [])
const isFocusInEditorOrToolbar = isInEditor || isInToolbar
const [isSelectionLink, setIsSelectionLink] = useState(false)
const [isSelectionAutoLink, setIsSelectionAutoLink] = useState(false)
const [linkUrl, setLinkUrl] = useState('')
const [isLinkEditMode, setIsLinkEditMode] = useState(false)
const [lastSelection, setLastSelection] = useState<RangeSelection | GridSelection | NodeSelection | null>(null)
const updateEditorSelection = useCallback(() => {
editor.getEditorState().read(() => {
const selection = $getSelection()
const nativeSelection = window.getSelection()
const activeElement = document.activeElement
const rootElement = editor.getRootElement()
if (!$isRangeSelection(selection)) {
return
}
const node = getSelectedNode(selection)
const parent = node.getParent()
if ($isLinkNode(parent) || $isLinkNode(node)) {
setIsSelectionLink(true)
} else {
setIsSelectionLink(false)
}
if ($isAutoLinkNode(parent) || $isAutoLinkNode(node)) {
setIsSelectionAutoLink(true)
} else {
setIsSelectionAutoLink(false)
}
if ($isLinkNode(parent)) {
setLinkUrl(parent.getURL())
} else if ($isLinkNode(node)) {
setLinkUrl(node.getURL())
} else {
setLinkUrl('')
}
if (
selection !== null &&
nativeSelection !== null &&
rootElement !== null &&
rootElement.contains(nativeSelection.anchorNode)
) {
setLastSelection(selection)
} else if (!activeElement || activeElement.id !== 'link-input') {
setLastSelection(null)
setIsLinkEditMode(false)
setLinkUrl('')
}
})
}, [editor])
useEffect(() => {
return mergeRegister(
editor.registerCommand(
SELECTION_CHANGE_COMMAND,
() => {
updateEditorSelection()
return false
},
COMMAND_PRIORITY_CRITICAL,
),
editor.registerUpdateListener(() => {
updateEditorSelection()
}),
)
}, [editor, updateEditorSelection])
const isFocusInEditorOrToolbar = isInEditor || isInToolbar || isInLinkEditor
if (!isMobile || !isFocusInEditorOrToolbar) {
return null
}
@@ -263,32 +357,46 @@ const MobileToolbarPlugin = () => {
return (
<>
{modal}
<div className="flex w-full flex-shrink-0 border-t border-border bg-contrast">
<div
tabIndex={-1}
className={classNames('flex items-center gap-1 overflow-x-auto', '[&::-webkit-scrollbar]:h-0')}
ref={toolbarRef}
>
{items.map((item) => {
return (
<button
className="flex items-center justify-center rounded px-3 py-3 disabled:opacity-50"
aria-label={item.name}
onClick={item.onSelect}
key={item.name}
disabled={item.disabled}
>
<Icon type={item.iconName} size="medium" className="!text-current [&>path]:!text-current" />
</button>
)
})}
<div className="bg-contrast" id="super-mobile-toolbar">
{isSelectionLink && (
<div className="border-t border-border px-2" ref={linkEditorRef}>
<LinkEditor
linkUrl={linkUrl}
isEditMode={isLinkEditMode}
setEditMode={setIsLinkEditMode}
isAutoLink={isSelectionAutoLink}
editor={editor}
lastSelection={lastSelection}
/>
</div>
)}
<div className="flex w-full flex-shrink-0 border-t border-border bg-contrast">
<div
tabIndex={-1}
className={classNames('flex items-center gap-1 overflow-x-auto', '[&::-webkit-scrollbar]:h-0')}
ref={toolbarRef}
>
{items.map((item) => {
return (
<button
className="flex items-center justify-center rounded px-3 py-3 disabled:opacity-50"
aria-label={item.name}
onClick={item.onSelect}
key={item.name}
disabled={item.disabled}
>
<Icon type={item.iconName} size="medium" className="!text-current [&>path]:!text-current" />
</button>
)
})}
</div>
<button
className="flex flex-shrink-0 items-center justify-center rounded border-l border-border px-3 py-3"
aria-label="Dismiss keyboard"
>
<Icon type="keyboard-close" size="medium" />
</button>
</div>
<button
className="flex flex-shrink-0 items-center justify-center rounded border-l border-border px-3 py-3"
aria-label="Dismiss keyboard"
>
<Icon type="keyboard-close" size="medium" />
</button>
</div>
</>
)
+157 -157
View File
@@ -2911,255 +2911,255 @@ __metadata:
languageName: node
linkType: hard
"@lexical/clipboard@npm:0.11.2":
version: 0.11.2
resolution: "@lexical/clipboard@npm:0.11.2"
"@lexical/clipboard@npm:0.11.3":
version: 0.11.3
resolution: "@lexical/clipboard@npm:0.11.3"
dependencies:
"@lexical/html": 0.11.2
"@lexical/list": 0.11.2
"@lexical/selection": 0.11.2
"@lexical/utils": 0.11.2
"@lexical/html": 0.11.3
"@lexical/list": 0.11.3
"@lexical/selection": 0.11.3
"@lexical/utils": 0.11.3
peerDependencies:
lexical: 0.11.2
checksum: ab102ac6bf803fe61e48b9ef19576af1db9fb39e0eb8af0501882337e69f41324243b8f95094f958f8181c1ef27816cba1c1eaaa71eb2d09e9b323ea0b6ee7f6
lexical: 0.11.3
checksum: 0a630b630197a8f7f8118049993c1aecee603272733c0c0e2a40e699f79efc11f93e8e1b65e8835a5ebbf22aa6af559dfe60f3d4b214cbea323d3902ed69a923
languageName: node
linkType: hard
"@lexical/code@npm:0.11.2":
version: 0.11.2
resolution: "@lexical/code@npm:0.11.2"
"@lexical/code@npm:0.11.3":
version: 0.11.3
resolution: "@lexical/code@npm:0.11.3"
dependencies:
"@lexical/utils": 0.11.2
"@lexical/utils": 0.11.3
prismjs: ^1.27.0
peerDependencies:
lexical: 0.11.2
checksum: af715151a741c1a74b8e5fe338329541c08635398c07b7c4078d8c8d8160be62416ac2c5995a032cad4308cf8db98deaf5029e1547c9a377660803e358e78ccb
lexical: 0.11.3
checksum: 35494f71c7f102c61a8737880762f64087b6345a1c069e8a13aa0c30a7ffaca433e192e70d1e891571e53cb3854e2ec18643615f18d693c9e3b0bf5cd6542624
languageName: node
linkType: hard
"@lexical/dragon@npm:0.11.2":
version: 0.11.2
resolution: "@lexical/dragon@npm:0.11.2"
"@lexical/dragon@npm:0.11.3":
version: 0.11.3
resolution: "@lexical/dragon@npm:0.11.3"
peerDependencies:
lexical: 0.11.2
checksum: 1926f48f08ededfee416c3ce6ffade01010156510fd8eb04b7c8d68f9d37b7196ce0356c4e5d1c863d582831a69204858d9df5d2804c606880b1afdcc5a7c903
lexical: 0.11.3
checksum: 8bafde2d36d67c04d2d21f45def17a7b21834260b2366c7ca0f6c5bb12d64ed5d5b24bf1190347fb03b5e10cc5f2b57e9726eba52ca1f5f517fcbb87eff7ab07
languageName: node
linkType: hard
"@lexical/hashtag@npm:0.11.2":
version: 0.11.2
resolution: "@lexical/hashtag@npm:0.11.2"
"@lexical/hashtag@npm:0.11.3":
version: 0.11.3
resolution: "@lexical/hashtag@npm:0.11.3"
dependencies:
"@lexical/utils": 0.11.2
"@lexical/utils": 0.11.3
peerDependencies:
lexical: 0.11.2
checksum: 34dbe2a803739239b0ab3f60dbb1ab7f13c30a7614d4e5a62c5b498a8a07babe014eda309786cde96a050e0afa056cad5f99e3748c4d8cb1d2a29dcd7480b57c
lexical: 0.11.3
checksum: db68da41653eb74765197b9630e1f78b7d1e5785b331f88f4578da707f44bf0afdcc6e076186400bac02dfda8c47751ecb57ad69a42cad59c79f9f8f346cd7d9
languageName: node
linkType: hard
"@lexical/headless@npm:0.11.2":
version: 0.11.2
resolution: "@lexical/headless@npm:0.11.2"
"@lexical/headless@npm:0.11.3":
version: 0.11.3
resolution: "@lexical/headless@npm:0.11.3"
peerDependencies:
lexical: 0.11.2
checksum: abc8b7602b29da48a767470a8dc0c680404214fc615f1689dc1188d17b1667391a13cbe53aa56b004047ba9f62dbeea912cb8e6d35871818fc666d0147e9c1bf
lexical: 0.11.3
checksum: 9bbabea45714a69deff7a6df7df55033c71904ffcee7398b8713e6178aace28f2fd75e111ca1f4113b13b2161498413ec82d90587f6289b35ab0de355494ebd2
languageName: node
linkType: hard
"@lexical/history@npm:0.11.2":
version: 0.11.2
resolution: "@lexical/history@npm:0.11.2"
"@lexical/history@npm:0.11.3":
version: 0.11.3
resolution: "@lexical/history@npm:0.11.3"
dependencies:
"@lexical/utils": 0.11.2
"@lexical/utils": 0.11.3
peerDependencies:
lexical: 0.11.2
checksum: a63562baa20cf11412004601978846da059b87506f64f498b2fcb5b1401f48c1e3048dc531aa3dcb813e1dd377164beeb651ac46180f618f3727838dfa89389f
lexical: 0.11.3
checksum: 59d828f647193f99ac6c8e04b8be7faa10f9862ff99efb5a24a2c4c5dd881b5da677a9531774f77737b9fe38015463588f5f6ea81d3d6c6ba5d47d5731ddd3c6
languageName: node
linkType: hard
"@lexical/html@npm:0.11.2":
version: 0.11.2
resolution: "@lexical/html@npm:0.11.2"
"@lexical/html@npm:0.11.3":
version: 0.11.3
resolution: "@lexical/html@npm:0.11.3"
dependencies:
"@lexical/selection": 0.11.2
"@lexical/selection": 0.11.3
peerDependencies:
lexical: 0.11.2
checksum: 32aa097370188fe825dc61fe7750ee2162f94cd8204e1f4f501eeecb7b437b7b1f22bc834a093cde49657a26c97bd57b3a4cd7c365a95c0365400389751fe8d3
lexical: 0.11.3
checksum: 08ecd189fc4ebc0c9e598658b07035f8905dd5ea8c254e01e4e655296453a4d03796f354fb9e8febdddadb8f3c29f57bbf2ef15a013d1a1bb69cf41853276936
languageName: node
linkType: hard
"@lexical/link@npm:0.11.2":
version: 0.11.2
resolution: "@lexical/link@npm:0.11.2"
"@lexical/link@npm:0.11.3":
version: 0.11.3
resolution: "@lexical/link@npm:0.11.3"
dependencies:
"@lexical/utils": 0.11.2
"@lexical/utils": 0.11.3
peerDependencies:
lexical: 0.11.2
checksum: 78a71053f73c3c659f786e3aa8a9e6332f019cf7d9e90ffbf64bd6466f842bb9a9dcba770454ed30fff646aa3d5abe016c80a7364b38349e0987a1891d9bec0b
lexical: 0.11.3
checksum: f0b91dcb00c7d2419763ba3ea0c02dff8dc669a343f7ef8aaeef5025a048556b223f501ad20c54f3848c936489eb74e41e51e3cbd1653f70d365fa6b80f87cc3
languageName: node
linkType: hard
"@lexical/list@npm:0.11.2":
version: 0.11.2
resolution: "@lexical/list@npm:0.11.2"
"@lexical/list@npm:0.11.3":
version: 0.11.3
resolution: "@lexical/list@npm:0.11.3"
dependencies:
"@lexical/utils": 0.11.2
"@lexical/utils": 0.11.3
peerDependencies:
lexical: 0.11.2
checksum: c94f35ec62a980c7af66582a4b1c9c0d8d7af3e4a9bdcff0323626882dd8a2a80ca0e2a8a212b09e6066741e207ebbeb743e0817950d14216ad55d9e22d8bd61
lexical: 0.11.3
checksum: 59bf95dc1f23c7a86a55caa435bc7861194ff271ea20444f888aaff201d3972722c43fb311e60b6cab9331910bb3afed45e71e7275555e0965d2af193c4aef60
languageName: node
linkType: hard
"@lexical/mark@npm:0.11.2":
version: 0.11.2
resolution: "@lexical/mark@npm:0.11.2"
"@lexical/mark@npm:0.11.3":
version: 0.11.3
resolution: "@lexical/mark@npm:0.11.3"
dependencies:
"@lexical/utils": 0.11.2
"@lexical/utils": 0.11.3
peerDependencies:
lexical: 0.11.2
checksum: e7edadd085ebcff1b4ad6c4467d2a63c6c417b48ac76bd63e64659d30e63d7ec5e86fc56f7a3480b50582f069cd6c81a8ea28afa2a4df96fb7dfab4dda878408
lexical: 0.11.3
checksum: e323b1d614975507b3bcc7ad6a5c48551c96c5986b09efc483914a1b1d2f481a1a99d6420042e992520e4f60827aa2b401ba22f60a7ef81d7f84387f241305f1
languageName: node
linkType: hard
"@lexical/markdown@npm:0.11.2":
version: 0.11.2
resolution: "@lexical/markdown@npm:0.11.2"
"@lexical/markdown@npm:0.11.3":
version: 0.11.3
resolution: "@lexical/markdown@npm:0.11.3"
dependencies:
"@lexical/code": 0.11.2
"@lexical/link": 0.11.2
"@lexical/list": 0.11.2
"@lexical/rich-text": 0.11.2
"@lexical/text": 0.11.2
"@lexical/utils": 0.11.2
"@lexical/code": 0.11.3
"@lexical/link": 0.11.3
"@lexical/list": 0.11.3
"@lexical/rich-text": 0.11.3
"@lexical/text": 0.11.3
"@lexical/utils": 0.11.3
peerDependencies:
lexical: 0.11.2
checksum: 89bb22134d3c1835a7936c7b2aa63df7d70cac8a369b4a5bdae96de72181d23db1ffe31c6d1b6e6d1b648a0bde6f177d665f48953a4ae43ab0eebaf6b3602d3e
lexical: 0.11.3
checksum: c43e2fff7ee7aad174e829923aa4df1bfd480ab1f645f24e22e8728bf5178995e31b465aed71e7e1aa82152aba748cf71cbf7f5e927da80d6e19d1a63e731485
languageName: node
linkType: hard
"@lexical/offset@npm:0.11.2":
version: 0.11.2
resolution: "@lexical/offset@npm:0.11.2"
"@lexical/offset@npm:0.11.3":
version: 0.11.3
resolution: "@lexical/offset@npm:0.11.3"
peerDependencies:
lexical: 0.11.2
checksum: 4d1093bc0afa4f2f51d45c4b36b21779e6fa2b17b9a8d5d95c9ca0482b8759aca026dba7fae5ac2d695e7e22d9262b828b9157b4a7a266a1f7861848cd3520a4
lexical: 0.11.3
checksum: bdc1f8fc040d2213d16233140e719b951932f3d22a6c7528aadd5867bca9be7e487cfa5c45d96ff7ce7bc1e4fabea17b4ce31dc331fede657323125ae01335e3
languageName: node
linkType: hard
"@lexical/overflow@npm:0.11.2":
version: 0.11.2
resolution: "@lexical/overflow@npm:0.11.2"
"@lexical/overflow@npm:0.11.3":
version: 0.11.3
resolution: "@lexical/overflow@npm:0.11.3"
peerDependencies:
lexical: 0.11.2
checksum: a6751df5984be024bfc60414e92bee1fc6336bf647234d30b4197b8bf972bbd0c81f61fd15449399fdc06f0de4ae03001431aacb90356097293cd149c8c07551
lexical: 0.11.3
checksum: d0f74582cc71bada93bb5c5653c7a425922d1bb8f69e782676ad2c52560a5d4a4f97ab4b2234d3047c9895ae8bdc4bed8f8d064ea23b756e2dbc4dafb2d4663c
languageName: node
linkType: hard
"@lexical/plain-text@npm:0.11.2":
version: 0.11.2
resolution: "@lexical/plain-text@npm:0.11.2"
"@lexical/plain-text@npm:0.11.3":
version: 0.11.3
resolution: "@lexical/plain-text@npm:0.11.3"
peerDependencies:
"@lexical/clipboard": 0.11.2
"@lexical/selection": 0.11.2
"@lexical/utils": 0.11.2
lexical: 0.11.2
checksum: ddc0ac63f19652134473d287279c7bac06948e78e0d9e2549d6fed8472b3eb40ff6966c0f358e4e085b46bcd1b95822bd6d865bc848e576aed0ec8339675517d
"@lexical/clipboard": 0.11.3
"@lexical/selection": 0.11.3
"@lexical/utils": 0.11.3
lexical: 0.11.3
checksum: 7d0929b87c6ba1658537163df6f8e58f0df7234e4b356aa612f793b845b594786235c78aaab59dbca93860f5d3d6a3b438622b368dfa270443857422fc811c66
languageName: node
linkType: hard
"@lexical/react@npm:0.11.2":
version: 0.11.2
resolution: "@lexical/react@npm:0.11.2"
"@lexical/react@npm:0.11.3":
version: 0.11.3
resolution: "@lexical/react@npm:0.11.3"
dependencies:
"@lexical/clipboard": 0.11.2
"@lexical/code": 0.11.2
"@lexical/dragon": 0.11.2
"@lexical/hashtag": 0.11.2
"@lexical/history": 0.11.2
"@lexical/link": 0.11.2
"@lexical/list": 0.11.2
"@lexical/mark": 0.11.2
"@lexical/markdown": 0.11.2
"@lexical/overflow": 0.11.2
"@lexical/plain-text": 0.11.2
"@lexical/rich-text": 0.11.2
"@lexical/selection": 0.11.2
"@lexical/table": 0.11.2
"@lexical/text": 0.11.2
"@lexical/utils": 0.11.2
"@lexical/yjs": 0.11.2
"@lexical/clipboard": 0.11.3
"@lexical/code": 0.11.3
"@lexical/dragon": 0.11.3
"@lexical/hashtag": 0.11.3
"@lexical/history": 0.11.3
"@lexical/link": 0.11.3
"@lexical/list": 0.11.3
"@lexical/mark": 0.11.3
"@lexical/markdown": 0.11.3
"@lexical/overflow": 0.11.3
"@lexical/plain-text": 0.11.3
"@lexical/rich-text": 0.11.3
"@lexical/selection": 0.11.3
"@lexical/table": 0.11.3
"@lexical/text": 0.11.3
"@lexical/utils": 0.11.3
"@lexical/yjs": 0.11.3
react-error-boundary: ^3.1.4
peerDependencies:
lexical: 0.11.2
lexical: 0.11.3
react: ">=17.x"
react-dom: ">=17.x"
checksum: 8fdc60712c248c9c242b717f39fe0c359b4db73a512b7ed7f7ac761061a789d4992cc533664f5ae6229384c4f426fe92f465f6b73b013c072dd3b7b057a5ec1d
checksum: a58c1ae508b9422a0ee542e62a28cbccf10b24b9029966de8b79c091cf4b697e9cfa23e05aa280a613d0dca3bd858480eb00ba5180868c55cc65381a0debcdec
languageName: node
linkType: hard
"@lexical/rich-text@npm:0.11.2":
version: 0.11.2
resolution: "@lexical/rich-text@npm:0.11.2"
"@lexical/rich-text@npm:0.11.3":
version: 0.11.3
resolution: "@lexical/rich-text@npm:0.11.3"
peerDependencies:
"@lexical/clipboard": 0.11.2
"@lexical/selection": 0.11.2
"@lexical/utils": 0.11.2
lexical: 0.11.2
checksum: 78d50a506b8431d33f99e1dc6041b68645f44791c6012490f5de62cc78214f546d8518ce495c8e887cf77ab57d010c8ff82841e5cb76bc4d61021da66cd1c9fd
"@lexical/clipboard": 0.11.3
"@lexical/selection": 0.11.3
"@lexical/utils": 0.11.3
lexical: 0.11.3
checksum: e3491a14f8a880e7fa0fad91c5e9a242a8d2977d1bbfec4ac807300249623c074c882dae30924c68c9f46263eaf816f1fb0a063180631bb045f0df1d4996b54a
languageName: node
linkType: hard
"@lexical/selection@npm:0.11.2":
version: 0.11.2
resolution: "@lexical/selection@npm:0.11.2"
"@lexical/selection@npm:0.11.3":
version: 0.11.3
resolution: "@lexical/selection@npm:0.11.3"
peerDependencies:
lexical: 0.11.2
checksum: 7b1f30331de349df9d5aab936b962676c1ffa8dd15b03049851c0bf8a232a0a747998b83656cf970279ea3e6f426e429ae5db2e0e4180c0a94f9f900c6c4070c
lexical: 0.11.3
checksum: e57ca9a08f1e302f62349245e44949f71235ac48e297cb0390b6dff3db4aeb475fced62e78d81692f1e2974a0766dd25c0f83e6130870e848de0f5f0554af4f5
languageName: node
linkType: hard
"@lexical/table@npm:0.11.2":
version: 0.11.2
resolution: "@lexical/table@npm:0.11.2"
"@lexical/table@npm:0.11.3":
version: 0.11.3
resolution: "@lexical/table@npm:0.11.3"
dependencies:
"@lexical/utils": 0.11.2
"@lexical/utils": 0.11.3
peerDependencies:
lexical: 0.11.2
checksum: 9ee3cd33631dcf85956990fe27489c6d2eee554505d4e0c5dd7a57c19f2d4fa878448bb3d7ae8083e4db633ed05d02f305f4c4a298b9dd7e499be4646b69613b
lexical: 0.11.3
checksum: d1aa2fc8d192830aec4fe2e8d2f6a64b4e061b70a618c166478ee889fc561c28487351e7dba8b7463053dc67c85faeb1f44cf0984ca3e15e6e41aa483063a40e
languageName: node
linkType: hard
"@lexical/text@npm:0.11.2":
version: 0.11.2
resolution: "@lexical/text@npm:0.11.2"
"@lexical/text@npm:0.11.3":
version: 0.11.3
resolution: "@lexical/text@npm:0.11.3"
peerDependencies:
lexical: 0.11.2
checksum: ee7e0859d7f4119a5bee9ee2f8255845ce32b16997cc353087976c8f65967e620494d31c63dc54b6001003534d0682418ccd407f61af53bd3bf7836da1d3b474
lexical: 0.11.3
checksum: 48b3e0e54f2dc08958872dd59ad4806c8fe20ab5bc832241ad7811ea36159366b58ea201b97cf96ce9c383f3b534aa1627b713bd3777cc3bff292a1a92bde6d2
languageName: node
linkType: hard
"@lexical/utils@npm:0.11.2":
version: 0.11.2
resolution: "@lexical/utils@npm:0.11.2"
"@lexical/utils@npm:0.11.3":
version: 0.11.3
resolution: "@lexical/utils@npm:0.11.3"
dependencies:
"@lexical/list": 0.11.2
"@lexical/selection": 0.11.2
"@lexical/table": 0.11.2
"@lexical/list": 0.11.3
"@lexical/selection": 0.11.3
"@lexical/table": 0.11.3
peerDependencies:
lexical: 0.11.2
checksum: 182f9150843b4ada726c31732afca2fa9465c9e08372fd6fbc91e676768a01866d3878247c12ee1f9a94f6397e3b0dfea462c0a35139bce174c773b95400c08b
lexical: 0.11.3
checksum: f7e185ee8f50398b7533a4f1e0f495c803a6ac4b350af30e1791013b63b2ee45465e65f6338ceea72bee894076fe9e46d33a40ae3c5e5fbaebda6eb3daf261b9
languageName: node
linkType: hard
"@lexical/yjs@npm:0.11.2":
version: 0.11.2
resolution: "@lexical/yjs@npm:0.11.2"
"@lexical/yjs@npm:0.11.3":
version: 0.11.3
resolution: "@lexical/yjs@npm:0.11.3"
dependencies:
"@lexical/offset": 0.11.2
"@lexical/offset": 0.11.3
peerDependencies:
lexical: 0.11.2
lexical: 0.11.3
yjs: ">=13.5.22"
checksum: a886ed415f845f9bd1e724dede42b10611f48b7bd6acef6781219e3d6220b3e984abcd7017e3ada636355558dd298c5cd4c202e0b87fb7f3c5fb36a494d7a4e9
checksum: f201fc07c863aea24043d9add6f3b98a871ef1811fca8a3ca90d6b36cc5aa56ab164728e9bbf007dd3401598bdc75fc68daf0b9bcb027b4fd7584da8b3d3e2d1
languageName: node
linkType: hard
@@ -5086,8 +5086,8 @@ __metadata:
"@babel/plugin-transform-react-jsx": ^7.19.0
"@babel/preset-env": "*"
"@babel/preset-typescript": ^7.21.5
"@lexical/headless": 0.11.2
"@lexical/react": 0.11.2
"@lexical/headless": 0.11.3
"@lexical/react": 0.11.3
"@pmmmwh/react-refresh-webpack-plugin": ^0.5.10
"@radix-ui/react-slot": ^1.0.1
"@simplewebauthn/browser": ^7.1.0
@@ -5137,7 +5137,7 @@ __metadata:
identity-obj-proxy: ^3.0.0
jest: ^29.3.1
jest-environment-jsdom: ^29.3.1
lexical: 0.11.2
lexical: 0.11.3
lint-staged: ">=13"
mini-css-extract-plugin: ^2.7.2
minimatch: ^5.1.1
@@ -15316,10 +15316,10 @@ __metadata:
languageName: node
linkType: hard
"lexical@npm:0.11.2":
version: 0.11.2
resolution: "lexical@npm:0.11.2"
checksum: 85ec66227b38e60c430e4a4be489b483022477fc4b815229fc96ab71e30b059f8ff443e2e10e2c68f1d731d856027e4e1a86a4fed2730fbde1267a0156676b84
"lexical@npm:0.11.3":
version: 0.11.3
resolution: "lexical@npm:0.11.3"
checksum: 89fa276db6e253c1b52f097155e6bd6402c4292845a37a0910b992c9051e339df5186d41f3f35eb3040ad8f322716c63c22b5e154b04e698814f5f4a02e49635
languageName: node
linkType: hard