Compare commits

..

12 Commits

Author SHA1 Message Date
standardci
4855e1d5f5 chore(release): publish new version
- @standardnotes/api-gateway@1.81.14
 - @standardnotes/home-server@1.18.32
2023-11-10 10:04:31 +00:00
Karol Sójko
5d3fb9a537 fix(api-gateway): add logs about calling web sockets with minimal format 2023-11-10 10:32:47 +01:00
standardci
b55d80a7cd chore(release): publish new version
- @standardnotes/api-gateway@1.81.13
 - @standardnotes/home-server@1.18.31
2023-11-09 14:29:28 +00:00
Karol Sójko
16f92bdc99 fix(api-gateway): add possibility to configure keep-alive timeout (#920) 2023-11-09 15:02:32 +01:00
standardci
4c5738416a chore(release): publish new version
- @standardnotes/home-server@1.18.30
 - @standardnotes/syncing-server@1.120.5
 - @standardnotes/websockets-server@1.17.8
2023-11-09 13:26:30 +00:00
Karol Sójko
45d4920e0f fix: remove unused axios dep in subservices 2023-11-09 14:03:44 +01:00
standardci
94e738532a chore(release): publish new version
- @standardnotes/api-gateway@1.81.12
 - @standardnotes/home-server@1.18.29
 - @standardnotes/websockets-server@1.17.7
2023-11-09 11:18:35 +00:00
Karol Sójko
c4ae12d53f fix: reduce websockets api communication data (#919) 2023-11-09 11:44:31 +01:00
standardci
4ff78452f9 chore(release): publish new version
- @standardnotes/auth-server@1.167.2
 - @standardnotes/home-server@1.18.28
 - @standardnotes/syncing-server@1.120.4
2023-11-08 15:38:47 +00:00
Karol Sójko
9465f2ecd8 fix: add logs about sending websocket events to clients 2023-11-08 16:10:44 +01:00
standardci
93c2f1f12f chore(release): publish new version
- @standardnotes/auth-server@1.167.1
 - @standardnotes/home-server@1.18.27
2023-11-08 12:15:35 +00:00
Karol Sójko
ca8a3fc77d fix(auth): path to delete accounts script 2023-11-08 12:30:12 +01:00
34 changed files with 185 additions and 719 deletions

2
.pnp.cjs generated
View File

@@ -6961,7 +6961,6 @@ const RAW_RUNTIME_STATE =
["@types/uuid", "npm:9.0.3"],\
["@typescript-eslint/eslint-plugin", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:6.5.0"],\
["@typescript-eslint/parser", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:6.5.0"],\
["axios", "npm:1.4.0"],\
["cors", "npm:2.8.5"],\
["dotenv", "npm:16.1.3"],\
["eslint", "npm:8.41.0"],\
@@ -7044,7 +7043,6 @@ const RAW_RUNTIME_STATE =
["@types/jest", "npm:29.5.2"],\
["@typescript-eslint/eslint-plugin", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:6.5.0"],\
["@typescript-eslint/parser", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:6.5.0"],\
["axios", "npm:1.4.0"],\
["cors", "npm:2.8.5"],\
["dotenv", "npm:16.1.3"],\
["eslint", "npm:8.41.0"],\

View File

@@ -3,6 +3,24 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.81.14](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.81.13...@standardnotes/api-gateway@1.81.14) (2023-11-10)
### Bug Fixes
* **api-gateway:** add logs about calling web sockets with minimal format ([5d3fb9a](https://github.com/standardnotes/api-gateway/commit/5d3fb9a537f6971cfe8ae3c5ea449806cc4de8a0))
## [1.81.13](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.81.12...@standardnotes/api-gateway@1.81.13) (2023-11-09)
### Bug Fixes
* **api-gateway:** add possibility to configure keep-alive timeout ([#920](https://github.com/standardnotes/api-gateway/issues/920)) ([16f92bd](https://github.com/standardnotes/api-gateway/commit/16f92bdc990ded5c3f1fe5af1e6e4a113a9954de))
## [1.81.12](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.81.11...@standardnotes/api-gateway@1.81.12) (2023-11-09)
### Bug Fixes
* reduce websockets api communication data ([#919](https://github.com/standardnotes/api-gateway/issues/919)) ([c4ae12d](https://github.com/standardnotes/api-gateway/commit/c4ae12d53fc166879f90a4c5dbad1ab1cb4797e2))
## [1.81.11](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.81.10...@standardnotes/api-gateway@1.81.11) (2023-11-07)
### Bug Fixes

View File

@@ -102,9 +102,11 @@ void container.load().then((container) => {
})
})
const serverInstance = server.build()
const serverInstance = server.build().listen(env.get('PORT'))
serverInstance.listen(env.get('PORT'))
const keepAliveTimeout = env.get('KEEP_ALIVE_TIMEOUT', true) ? +env.get('KEEP_ALIVE_TIMEOUT', true) : 5000
serverInstance.keepAliveTimeout = keepAliveTimeout
logger.info(`Server started on port ${process.env.PORT}`)
})

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/api-gateway",
"version": "1.81.11",
"version": "1.81.14",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -143,7 +143,21 @@ export class HttpServiceProxy implements ServiceProxyInterface {
return
}
await this.callServer(this.webSocketServerUrl, request, response, endpointOrMethodIdentifier, payload)
const isARequestComingFromApiGatewayAndShouldBeKeptInMinimalFormat = request.headers.connectionid !== undefined
this.logger.info(
`Calling websockets service: ${endpointOrMethodIdentifier}. Format is minimal: ${isARequestComingFromApiGatewayAndShouldBeKeptInMinimalFormat}`,
)
if (isARequestComingFromApiGatewayAndShouldBeKeptInMinimalFormat) {
await this.callServerWithLegacyFormat(
this.webSocketServerUrl,
request,
response,
endpointOrMethodIdentifier,
payload,
)
} else {
await this.callServer(this.webSocketServerUrl, request, response, endpointOrMethodIdentifier, payload)
}
}
async callPaymentsServer(

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.167.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.167.1...@standardnotes/auth-server@1.167.2) (2023-11-08)
### Bug Fixes
* add logs about sending websocket events to clients ([9465f2e](https://github.com/standardnotes/server/commit/9465f2ecd8e8f0bf3ebeeb3976227b1b105aded0))
## [1.167.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.167.0...@standardnotes/auth-server@1.167.1) (2023-11-08)
### Bug Fixes
* **auth:** path to delete accounts script ([ca8a3fc](https://github.com/standardnotes/server/commit/ca8a3fc77d91410f0dee8c3ddef29c09947c9cf5))
# [1.167.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.166.0...@standardnotes/auth-server@1.167.0) (2023-11-08)
### Features

View File

@@ -4,7 +4,7 @@ const path = require('path')
const pnp = require(path.normalize(path.resolve(__dirname, '../../..', '.pnp.cjs'))).setup()
const index = require(path.normalize(path.resolve(__dirname, '../dist/bin/delete-accounts.js')))
const index = require(path.normalize(path.resolve(__dirname, '../dist/bin/delete_accounts.js')))
Object.defineProperty(exports, '__esModule', { value: true })

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/auth-server",
"version": "1.167.0",
"version": "1.167.2",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

View File

@@ -5,12 +5,14 @@ import { DomainEventFactoryInterface } from '../../Domain/Event/DomainEventFacto
import { User } from '../../Domain/User/User'
import { ClientServiceInterface } from '../../Domain/Client/ClientServiceInterface'
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { Logger } from 'winston'
@injectable()
export class WebSocketsClientService implements ClientServiceInterface {
constructor(
@inject(TYPES.Auth_DomainEventFactory) private domainEventFactory: DomainEventFactoryInterface,
@inject(TYPES.Auth_DomainEventPublisher) private domainEventPublisher: DomainEventPublisherInterface,
@inject(TYPES.Auth_Logger) private logger: Logger,
) {}
async sendUserRolesChangedEvent(user: User): Promise<void> {
@@ -20,6 +22,8 @@ export class WebSocketsClientService implements ClientServiceInterface {
(await user.roles).map((role) => role.name),
)
this.logger.info(`[WebSockets] Requesting message ${event.type} to user ${user.uuid}`)
await this.domainEventPublisher.publish(
this.domainEventFactory.createWebSocketMessageRequestedEvent({
userUuid: user.uuid,

View File

@@ -3,6 +3,30 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.18.32](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.18.31...@standardnotes/home-server@1.18.32) (2023-11-10)
**Note:** Version bump only for package @standardnotes/home-server
## [1.18.31](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.18.30...@standardnotes/home-server@1.18.31) (2023-11-09)
**Note:** Version bump only for package @standardnotes/home-server
## [1.18.30](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.18.29...@standardnotes/home-server@1.18.30) (2023-11-09)
**Note:** Version bump only for package @standardnotes/home-server
## [1.18.29](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.18.28...@standardnotes/home-server@1.18.29) (2023-11-09)
**Note:** Version bump only for package @standardnotes/home-server
## [1.18.28](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.18.27...@standardnotes/home-server@1.18.28) (2023-11-08)
**Note:** Version bump only for package @standardnotes/home-server
## [1.18.27](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.18.26...@standardnotes/home-server@1.18.27) (2023-11-08)
**Note:** Version bump only for package @standardnotes/home-server
## [1.18.26](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.18.25...@standardnotes/home-server@1.18.26) (2023-11-08)
**Note:** Version bump only for package @standardnotes/home-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/home-server",
"version": "1.18.26",
"version": "1.18.32",
"engines": {
"node": ">=18.0.0 <21.0.0"
},

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.120.5](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.120.4...@standardnotes/syncing-server@1.120.5) (2023-11-09)
### Bug Fixes
* remove unused axios dep in subservices ([45d4920](https://github.com/standardnotes/syncing-server-js/commit/45d4920e0fc2848a28ce888d139201e68c4b416f))
## [1.120.4](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.120.3...@standardnotes/syncing-server@1.120.4) (2023-11-08)
### Bug Fixes
* add logs about sending websocket events to clients ([9465f2e](https://github.com/standardnotes/syncing-server-js/commit/9465f2ecd8e8f0bf3ebeeb3976227b1b105aded0))
## [1.120.3](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.120.2...@standardnotes/syncing-server@1.120.3) (2023-11-07)
**Note:** Version bump only for package @standardnotes/syncing-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/syncing-server",
"version": "1.120.3",
"version": "1.120.5",
"engines": {
"node": ">=18.0.0 <21.0.0"
},
@@ -40,7 +40,6 @@
"@standardnotes/settings": "workspace:*",
"@standardnotes/sncrypto-node": "workspace:*",
"@standardnotes/time": "workspace:*",
"axios": "^1.1.3",
"cors": "2.8.5",
"dotenv": "^16.0.1",
"express": "^4.18.2",

View File

@@ -44,9 +44,6 @@ import {
DomainEventPublisherInterface,
DomainEventSubscriberInterface,
} from '@standardnotes/domain-events'
import axios, { AxiosInstance } from 'axios'
import { ExtensionsHttpService } from '../Domain/Extension/ExtensionsHttpService'
import { ExtensionsHttpServiceInterface } from '../Domain/Extension/ExtensionsHttpServiceInterface'
import { AccountDeletionRequestedEventHandler } from '../Domain/Handler/AccountDeletionRequestedEventHandler'
import { DuplicateItemSyncedEventHandler } from '../Domain/Handler/DuplicateItemSyncedEventHandler'
import { EmailBackupRequestedEventHandler } from '../Domain/Handler/EmailBackupRequestedEventHandler'
@@ -554,6 +551,7 @@ export class ContainerConfigLoader {
new SendEventToClient(
container.get<DomainEventFactoryInterface>(TYPES.Sync_DomainEventFactory),
container.get<DomainEventPublisherInterface>(TYPES.Sync_DomainEventPublisher),
container.get<Logger>(TYPES.Sync_Logger),
),
)
container
@@ -948,20 +946,7 @@ export class ContainerConfigLoader {
)
// Services
container.bind<ContentDecoder>(TYPES.Sync_ContentDecoder).toDynamicValue(() => new ContentDecoder())
container.bind<AxiosInstance>(TYPES.Sync_HTTPClient).toDynamicValue(() => axios.create())
container
.bind<ExtensionsHttpServiceInterface>(TYPES.Sync_ExtensionsHttpService)
.toConstantValue(
new ExtensionsHttpService(
container.get<AxiosInstance>(TYPES.Sync_HTTPClient),
container.get<ItemRepositoryInterface>(TYPES.Sync_SQLItemRepository),
container.get<ContentDecoderInterface>(TYPES.Sync_ContentDecoder),
container.get<DomainEventPublisherInterface>(TYPES.Sync_DomainEventPublisher),
container.get<DomainEventFactoryInterface>(TYPES.Sync_DomainEventFactory),
container.get<Logger>(TYPES.Sync_Logger),
),
)
container.bind<ContentDecoderInterface>(TYPES.Sync_ContentDecoder).toDynamicValue(() => new ContentDecoder())
const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
['DUPLICATE_ITEM_SYNCED', container.get(TYPES.Sync_DuplicateItemSyncedEventHandler)],

View File

@@ -104,7 +104,6 @@ const TYPES = {
Sync_SyncResponseFactory20161215: Symbol.for('Sync_SyncResponseFactory20161215'),
Sync_SyncResponseFactory20200115: Symbol.for('Sync_SyncResponseFactory20200115'),
Sync_SyncResponseFactoryResolver: Symbol.for('Sync_SyncResponseFactoryResolver'),
Sync_ExtensionsHttpService: Symbol.for('Sync_ExtensionsHttpService'),
Sync_ItemBackupService: Symbol.for('Sync_ItemBackupService'),
Sync_ItemSaveValidator: Symbol.for('Sync_ItemSaveValidator'),
Sync_OwnershipFilter: Symbol.for('Sync_OwnershipFilter'),

View File

@@ -1,5 +0,0 @@
export enum ExtensionName {
Dropbox = 'Dropbox',
GoogleDrive = 'Google Drive',
OneDrive = 'OneDrive',
}

View File

@@ -1,438 +0,0 @@
import 'reflect-metadata'
import { KeyParamsData } from '@standardnotes/responses'
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { Logger } from 'winston'
import { ContentDecoderInterface } from '../Item/ContentDecoderInterface'
import { Item } from '../Item/Item'
import { ItemRepositoryInterface } from '../Item/ItemRepositoryInterface'
import { ExtensionsHttpService } from './ExtensionsHttpService'
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
import { AxiosInstance } from 'axios'
import { Uuid, ContentType, Dates, Timestamps, UniqueEntityId } from '@standardnotes/domain-core'
describe('ExtensionsHttpService', () => {
let httpClient: AxiosInstance
let primaryItemRepository: ItemRepositoryInterface
let contentDecoder: ContentDecoderInterface
let domainEventPublisher: DomainEventPublisherInterface
let domainEventFactory: DomainEventFactoryInterface
let item: Item
let authParams: KeyParamsData
let logger: Logger
const createService = () =>
new ExtensionsHttpService(
httpClient,
primaryItemRepository,
contentDecoder,
domainEventPublisher,
domainEventFactory,
logger,
)
beforeEach(() => {
httpClient = {} as jest.Mocked<AxiosInstance>
httpClient.request = jest.fn().mockReturnValue({ status: 200, data: { foo: 'bar' } })
item = Item.create(
{
userUuid: Uuid.create('00000000-0000-0000-0000-000000000000').getValue(),
updatedWithSession: null,
content: 'foobar',
contentType: ContentType.create(ContentType.TYPES.Note).getValue(),
encItemKey: null,
authHash: null,
itemsKeyId: null,
duplicateOf: null,
deleted: false,
dates: Dates.create(new Date(1616164633241311), new Date(1616164633241311)).getValue(),
timestamps: Timestamps.create(1616164633241311, 1616164633241311).getValue(),
},
new UniqueEntityId('00000000-0000-0000-0000-000000000000'),
).getValue()
authParams = {} as jest.Mocked<KeyParamsData>
primaryItemRepository = {} as jest.Mocked<ItemRepositoryInterface>
primaryItemRepository.findByUuidAndUserUuid = jest.fn().mockReturnValue(item)
logger = {} as jest.Mocked<Logger>
logger.error = jest.fn()
domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
domainEventPublisher.publish = jest.fn()
domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
domainEventFactory.createEmailRequestedEvent = jest.fn()
contentDecoder = {} as jest.Mocked<ContentDecoderInterface>
contentDecoder.decode = jest.fn().mockReturnValue({ name: 'Dropbox' })
})
it('should trigger cloud backup on extensions server', async () => {
await createService().triggerCloudBackupOnExtensionsServer({
userUuid: '1-2-3',
extensionsServerUrl: 'https://extensions-server/extension1',
forceMute: false,
backupFilename: 'test',
authParams,
cloudProvider: 'DROPBOX',
})
expect(httpClient.request).toHaveBeenCalledWith({
data: {
auth_params: authParams,
backup_filename: 'test',
silent: false,
user_uuid: '1-2-3',
},
headers: {
'Content-Type': 'application/json',
},
method: 'POST',
url: 'https://extensions-server/extension1',
validateStatus: expect.any(Function),
})
})
it('should publish a failed Dropbox backup event if request was not sent successfully', async () => {
contentDecoder.decode = jest.fn().mockReturnValue({ name: 'Dropbox' })
httpClient.request = jest.fn().mockImplementation(() => {
throw new Error('Could not reach the extensions server')
})
await createService().triggerCloudBackupOnExtensionsServer({
userUuid: '1-2-3',
extensionsServerUrl: 'https://extensions-server/extension1',
forceMute: false,
backupFilename: 'test',
authParams,
cloudProvider: 'DROPBOX',
})
expect(domainEventPublisher.publish).toHaveBeenCalled()
expect(domainEventFactory.createEmailRequestedEvent).toHaveBeenCalled()
})
it('should send items to extensions server', async () => {
await createService().sendItemsToExtensionsServer({
userUuid: '1-2-3',
extensionId: '2-3-4',
extensionsServerUrl: 'https://extensions-server/extension1',
forceMute: false,
items: [item],
backupFilename: '',
authParams,
})
expect(httpClient.request).toHaveBeenCalledWith({
data: {
auth_params: authParams,
backup_filename: '',
items: [item],
silent: false,
user_uuid: '1-2-3',
},
headers: {
'Content-Type': 'application/json',
},
method: 'POST',
url: 'https://extensions-server/extension1',
validateStatus: expect.any(Function),
})
})
it('should send items proxy backup file name only to extensions server', async () => {
await createService().sendItemsToExtensionsServer({
userUuid: '1-2-3',
extensionId: '2-3-4',
extensionsServerUrl: 'https://extensions-server/extension1',
forceMute: false,
backupFilename: 'backup-file',
authParams,
})
expect(httpClient.request).toHaveBeenCalledWith({
data: {
auth_params: authParams,
backup_filename: 'backup-file',
silent: false,
user_uuid: '1-2-3',
},
headers: {
'Content-Type': 'application/json',
},
method: 'POST',
url: 'https://extensions-server/extension1',
validateStatus: expect.any(Function),
})
})
it('should publish a failed Dropbox backup event if request was not sent successfully', async () => {
contentDecoder.decode = jest.fn().mockReturnValue({ name: 'Dropbox' })
httpClient.request = jest.fn().mockImplementation(() => {
throw new Error('Could not reach the extensions server')
})
await createService().sendItemsToExtensionsServer({
userUuid: '1-2-3',
extensionId: '2-3-4',
extensionsServerUrl: 'https://extensions-server/extension1',
forceMute: false,
items: [item],
backupFilename: 'backup-file',
authParams,
})
expect(domainEventPublisher.publish).toHaveBeenCalled()
expect(domainEventFactory.createEmailRequestedEvent).toHaveBeenCalled()
})
it('should publish a failed Dropbox backup event if request was sent and extensions server responded not ok', async () => {
contentDecoder.decode = jest.fn().mockReturnValue({ name: 'Dropbox' })
httpClient.request = jest.fn().mockReturnValue({ status: 400, data: { error: 'foo-bar' } })
await createService().sendItemsToExtensionsServer({
userUuid: '1-2-3',
extensionId: '2-3-4',
extensionsServerUrl: 'https://extensions-server/extension1',
forceMute: false,
items: [item],
backupFilename: 'backup-file',
authParams,
})
expect(domainEventPublisher.publish).toHaveBeenCalled()
expect(domainEventFactory.createEmailRequestedEvent).toHaveBeenCalled()
})
it('should publish a failed Google Drive backup event if request was not sent successfully', async () => {
contentDecoder.decode = jest.fn().mockReturnValue({ name: 'Google Drive' })
httpClient.request = jest.fn().mockImplementation(() => {
throw new Error('Could not reach the extensions server')
})
await createService().sendItemsToExtensionsServer({
userUuid: '1-2-3',
extensionId: '2-3-4',
extensionsServerUrl: 'https://extensions-server/extension1',
forceMute: false,
items: [item],
backupFilename: 'backup-file',
authParams,
})
expect(domainEventPublisher.publish).toHaveBeenCalled()
expect(domainEventFactory.createEmailRequestedEvent).toHaveBeenCalled()
})
it('should publish a failed One Drive backup event if request was not sent successfully', async () => {
contentDecoder.decode = jest.fn().mockReturnValue({ name: 'OneDrive' })
httpClient.request = jest.fn().mockImplementation(() => {
throw new Error('Could not reach the extensions server')
})
await createService().sendItemsToExtensionsServer({
userUuid: '1-2-3',
extensionId: '2-3-4',
extensionsServerUrl: 'https://extensions-server/extension1',
forceMute: false,
items: [item],
backupFilename: 'backup-file',
authParams,
})
expect(domainEventPublisher.publish).toHaveBeenCalled()
expect(domainEventFactory.createEmailRequestedEvent).toHaveBeenCalled()
})
it('should not publish a failed backup event if emailes are force muted', async () => {
contentDecoder.decode = jest.fn().mockReturnValue({ name: 'OneDrive' })
httpClient.request = jest.fn().mockImplementation(() => {
throw new Error('Could not reach the extensions server')
})
await createService().sendItemsToExtensionsServer({
userUuid: '1-2-3',
extensionId: '2-3-4',
extensionsServerUrl: 'https://extensions-server/extension1',
forceMute: true,
items: [item],
backupFilename: 'backup-file',
authParams,
})
expect(domainEventPublisher.publish).not.toHaveBeenCalled()
})
it('should throw an error if the extension to post to is not found', async () => {
primaryItemRepository.findByUuidAndUserUuid = jest.fn().mockReturnValue(null)
httpClient.request = jest.fn().mockImplementation(() => {
throw new Error('Could not reach the extensions server')
})
let error = null
try {
await createService().sendItemsToExtensionsServer({
userUuid: '1-2-3',
extensionId: '2-3-4',
extensionsServerUrl: 'https://extensions-server/extension1',
forceMute: false,
items: [item],
backupFilename: 'backup-file',
authParams,
})
} catch (e) {
error = e
}
expect(error).not.toBeNull()
})
it('should throw an error if the extension to post to has no content', async () => {
item = {} as jest.Mocked<Item>
primaryItemRepository.findByUuidAndUserUuid = jest.fn().mockReturnValue(item)
httpClient.request = jest.fn().mockImplementation(() => {
throw new Error('Could not reach the extensions server')
})
let error = null
try {
await createService().sendItemsToExtensionsServer({
userUuid: '1-2-3',
extensionId: '2-3-4',
extensionsServerUrl: 'https://extensions-server/extension1',
forceMute: false,
items: [item],
backupFilename: 'backup-file',
authParams,
})
} catch (e) {
error = e
}
expect(error).not.toBeNull()
})
it('should publish a failed Dropbox backup event judging by extension url if request was not sent successfully', async () => {
contentDecoder.decode = jest.fn().mockReturnValue({ url: 'https://dbt.com/...' })
httpClient.request = jest.fn().mockImplementation(() => {
throw new Error('Could not reach the extensions server')
})
await createService().sendItemsToExtensionsServer({
userUuid: '1-2-3',
extensionId: '2-3-4',
extensionsServerUrl: 'https://extensions-server/extension1',
forceMute: false,
items: [item],
backupFilename: 'backup-file',
authParams,
})
expect(domainEventPublisher.publish).toHaveBeenCalled()
expect(domainEventFactory.createEmailRequestedEvent).toHaveBeenCalled()
})
it('should publish a failed Google Drive backup event judging by extension url if request was not sent successfully', async () => {
contentDecoder.decode = jest.fn().mockReturnValue({ url: 'https://gdrive.com/...' })
httpClient.request = jest.fn().mockImplementation(() => {
throw new Error('Could not reach the extensions server')
})
await createService().sendItemsToExtensionsServer({
userUuid: '1-2-3',
extensionId: '2-3-4',
extensionsServerUrl: 'https://extensions-server/extension1',
forceMute: false,
items: [item],
backupFilename: 'backup-file',
authParams,
})
expect(domainEventPublisher.publish).toHaveBeenCalled()
expect(domainEventFactory.createEmailRequestedEvent).toHaveBeenCalled()
})
it('should publish a failed One Drive backup event judging by extension url if request was not sent successfully', async () => {
contentDecoder.decode = jest.fn().mockReturnValue({ url: 'https://onedrive.com/...' })
httpClient.request = jest.fn().mockImplementation(() => {
throw new Error('Could not reach the extensions server')
})
await createService().sendItemsToExtensionsServer({
userUuid: '1-2-3',
extensionId: '2-3-4',
extensionsServerUrl: 'https://extensions-server/extension1',
forceMute: false,
items: [item],
backupFilename: 'backup-file',
authParams,
})
expect(domainEventPublisher.publish).toHaveBeenCalled()
expect(domainEventFactory.createEmailRequestedEvent).toHaveBeenCalled()
})
it('should throw an error if cannot deduce extension by judging from the url', async () => {
contentDecoder.decode = jest.fn().mockReturnValue({ url: 'https://foobar.com/...' })
httpClient.request = jest.fn().mockImplementation(() => {
throw new Error('Could not reach the extensions server')
})
let error = null
try {
await createService().sendItemsToExtensionsServer({
userUuid: '1-2-3',
extensionId: '2-3-4',
extensionsServerUrl: 'https://extensions-server/extension1',
forceMute: false,
items: [item],
backupFilename: 'backup-file',
authParams,
})
} catch (e) {
error = e
}
expect(error).not.toBeNull()
})
it('should throw an error if there is no extension name or url', async () => {
contentDecoder.decode = jest.fn().mockReturnValue({})
httpClient.request = jest.fn().mockImplementation(() => {
throw new Error('Could not reach the extensions server')
})
let error = null
try {
await createService().sendItemsToExtensionsServer({
userUuid: '1-2-3',
extensionId: '2-3-4',
extensionsServerUrl: 'https://extensions-server/extension1',
forceMute: false,
items: [item],
backupFilename: 'backup-file',
authParams,
})
} catch (e) {
error = e
}
expect(error).not.toBeNull()
})
})

View File

@@ -1,177 +0,0 @@
import { KeyParamsData } from '@standardnotes/responses'
import { DomainEventInterface, DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { EmailLevel } from '@standardnotes/domain-core'
import { AxiosInstance } from 'axios'
import { Logger } from 'winston'
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
import { ContentDecoderInterface } from '../Item/ContentDecoderInterface'
import { ItemRepositoryInterface } from '../Item/ItemRepositoryInterface'
import { ExtensionName } from './ExtensionName'
import { ExtensionsHttpServiceInterface } from './ExtensionsHttpServiceInterface'
import { SendItemsToExtensionsServerDTO } from './SendItemsToExtensionsServerDTO'
import { getBody as googleDriveBody, getSubject as googleDriveSubject } from '../Email/GoogleDriveBackupFailed'
import { getBody as dropboxBody, getSubject as dropboxSubject } from '../Email/DropboxBackupFailed'
import { getBody as oneDriveBody, getSubject as oneDriveSubject } from '../Email/OneDriveBackupFailed'
export class ExtensionsHttpService implements ExtensionsHttpServiceInterface {
constructor(
private httpClient: AxiosInstance,
private primaryItemRepository: ItemRepositoryInterface,
private contentDecoder: ContentDecoderInterface,
private domainEventPublisher: DomainEventPublisherInterface,
private domainEventFactory: DomainEventFactoryInterface,
private logger: Logger,
) {}
async triggerCloudBackupOnExtensionsServer(dto: {
cloudProvider: 'DROPBOX' | 'GOOGLE_DRIVE' | 'ONE_DRIVE'
extensionsServerUrl: string
backupFilename: string
authParams: KeyParamsData
forceMute: boolean
userUuid: string
}): Promise<void> {
let sent = false
try {
const payload: Record<string, unknown> = {
backup_filename: dto.backupFilename,
auth_params: dto.authParams,
silent: dto.forceMute,
user_uuid: dto.userUuid,
}
const response = await this.httpClient.request({
method: 'POST',
url: dto.extensionsServerUrl,
headers: {
'Content-Type': 'application/json',
},
data: payload,
validateStatus:
/* istanbul ignore next */
(status: number) => status >= 200 && status < 500,
})
sent = response.status >= 200 && response.status < 300
} catch (error) {
this.logger.error(`[${dto.userUuid}] Failed to send a request to extensions server: ${(error as Error).message}`)
}
if (!sent && !dto.forceMute) {
await this.domainEventPublisher.publish(
this.createCloudBackupFailedEventBasedOnProvider(dto.cloudProvider, dto.authParams.identifier as string),
)
}
}
async sendItemsToExtensionsServer(dto: SendItemsToExtensionsServerDTO): Promise<void> {
let sent = false
try {
const payload: Record<string, unknown> = {
backup_filename: dto.backupFilename,
auth_params: dto.authParams,
silent: dto.forceMute,
user_uuid: dto.userUuid,
}
if (dto.items !== undefined) {
payload.items = dto.items
}
const response = await this.httpClient.request({
method: 'POST',
url: dto.extensionsServerUrl,
headers: {
'Content-Type': 'application/json',
},
data: payload,
validateStatus:
/* istanbul ignore next */
(status: number) => status >= 200 && status < 500,
})
sent = response.status >= 200 && response.status < 300
} catch (error) {
this.logger.error(`[${dto.userUuid}] Failed to send a request to extensions server: ${(error as Error).message}`)
}
if (!sent && !dto.forceMute) {
await this.domainEventPublisher.publish(
await this.getBackupFailedEvent(dto.extensionId, dto.userUuid, dto.authParams.identifier as string),
)
}
}
private createCloudBackupFailedEventBasedOnProvider(
cloudProvider: 'DROPBOX' | 'GOOGLE_DRIVE' | 'ONE_DRIVE',
email: string,
): DomainEventInterface {
switch (cloudProvider) {
case 'DROPBOX':
return this.domainEventFactory.createEmailRequestedEvent({
userEmail: email,
level: EmailLevel.LEVELS.FailedCloudBackup,
body: dropboxBody(),
messageIdentifier: 'FAILED_DROPBOX_BACKUP',
subject: dropboxSubject(),
})
case 'GOOGLE_DRIVE':
return this.domainEventFactory.createEmailRequestedEvent({
userEmail: email,
level: EmailLevel.LEVELS.FailedCloudBackup,
body: googleDriveBody(),
messageIdentifier: 'FAILED_GOOGLE_DRIVE_BACKUP',
subject: googleDriveSubject(),
})
case 'ONE_DRIVE':
return this.domainEventFactory.createEmailRequestedEvent({
userEmail: email,
level: EmailLevel.LEVELS.FailedCloudBackup,
body: oneDriveBody(),
messageIdentifier: 'FAILED_ONE_DRIVE_BACKUP',
subject: oneDriveSubject(),
})
}
}
private async getBackupFailedEvent(
extensionId: string,
userUuid: string,
email: string,
): Promise<DomainEventInterface> {
const extension = await this.primaryItemRepository.findByUuidAndUserUuid(extensionId, userUuid)
if (extension === null || !extension.props.content) {
throw Error(`Could not find extensions with id ${extensionId}`)
}
const content = this.contentDecoder.decode(extension.props.content)
switch (this.getExtensionName(content)) {
case ExtensionName.Dropbox:
return this.createCloudBackupFailedEventBasedOnProvider('DROPBOX', email)
case ExtensionName.GoogleDrive:
return this.createCloudBackupFailedEventBasedOnProvider('GOOGLE_DRIVE', email)
case ExtensionName.OneDrive:
return this.createCloudBackupFailedEventBasedOnProvider('ONE_DRIVE', email)
}
}
private getExtensionName(content: Record<string, unknown>): ExtensionName {
if ('name' in content) {
return <ExtensionName>content.name
}
const url = 'url' in content ? <string>content.url : undefined
if (url) {
if (url.indexOf('dbt') !== -1) {
return ExtensionName.Dropbox
} else if (url.indexOf('gdrive') !== -1) {
return ExtensionName.GoogleDrive
} else if (url.indexOf('onedrive') !== -1) {
return ExtensionName.OneDrive
}
}
throw Error('Could not deduce extension name from extension content')
}
}

View File

@@ -1,15 +0,0 @@
import { KeyParamsData } from '@standardnotes/responses'
import { SendItemsToExtensionsServerDTO } from './SendItemsToExtensionsServerDTO'
export interface ExtensionsHttpServiceInterface {
triggerCloudBackupOnExtensionsServer(dto: {
cloudProvider: 'DROPBOX' | 'GOOGLE_DRIVE' | 'ONE_DRIVE'
extensionsServerUrl: string
backupFilename: string
authParams: KeyParamsData
forceMute: boolean
userUuid: string
muteEmailsSettingUuid: string
}): Promise<void>
sendItemsToExtensionsServer(dto: SendItemsToExtensionsServerDTO): Promise<void>
}

View File

@@ -1,13 +0,0 @@
import { KeyParamsData } from '@standardnotes/responses'
import { Item } from '../Item/Item'
export type SendItemsToExtensionsServerDTO = {
extensionsServerUrl: string
extensionId: string
backupFilename: string
authParams: KeyParamsData
forceMute: boolean
userUuid: string
items?: Array<Item>
}

View File

@@ -5,14 +5,19 @@ import {
} from '@standardnotes/domain-events'
import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface'
import { SendEventToClient } from './SendEventToClient'
import { Logger } from 'winston'
describe('SendEventToClient', () => {
let domainEventFactory: DomainEventFactoryInterface
let domainEventPublisher: DomainEventPublisherInterface
let logger: Logger
const createUseCase = () => new SendEventToClient(domainEventFactory, domainEventPublisher)
const createUseCase = () => new SendEventToClient(domainEventFactory, domainEventPublisher, logger)
beforeEach(() => {
logger = {} as jest.Mocked<Logger>
logger.info = jest.fn()
domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
domainEventFactory.createWebSocketMessageRequestedEvent = jest
.fn()

View File

@@ -3,11 +3,13 @@ import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { SendEventToClientDTO } from './SendEventToClientDTO'
import { DomainEventFactoryInterface } from '../../../Event/DomainEventFactoryInterface'
import { Logger } from 'winston'
export class SendEventToClient implements UseCaseInterface<void> {
constructor(
private domainEventFactory: DomainEventFactoryInterface,
private domainEventPublisher: DomainEventPublisherInterface,
private logger: Logger,
) {}
async execute(dto: SendEventToClientDTO): Promise<Result<void>> {
@@ -17,6 +19,8 @@ export class SendEventToClient implements UseCaseInterface<void> {
}
const userUuid = userUuidOrError.getValue()
this.logger.info(`[WebSockets] Requesting message ${dto.event.type} to user ${dto.userUuid}`)
const event = this.domainEventFactory.createWebSocketMessageRequestedEvent({
userUuid: userUuid.value,
message: JSON.stringify(dto.event),

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.17.8](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.17.7...@standardnotes/websockets-server@1.17.8) (2023-11-09)
### Bug Fixes
* remove unused axios dep in subservices ([45d4920](https://github.com/standardnotes/server/commit/45d4920e0fc2848a28ce888d139201e68c4b416f))
## [1.17.7](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.17.6...@standardnotes/websockets-server@1.17.7) (2023-11-09)
### Bug Fixes
* reduce websockets api communication data ([#919](https://github.com/standardnotes/server/issues/919)) ([c4ae12d](https://github.com/standardnotes/server/commit/c4ae12d53fc166879f90a4c5dbad1ab1cb4797e2))
## [1.17.6](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.17.5...@standardnotes/websockets-server@1.17.6) (2023-11-07)
**Note:** Version bump only for package @standardnotes/websockets-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/websockets-server",
"version": "1.17.6",
"version": "1.17.8",
"engines": {
"node": ">=18.0.0 <21.0.0"
},
@@ -31,7 +31,6 @@
"@standardnotes/domain-events-infra": "workspace:^",
"@standardnotes/responses": "^1.13.27",
"@standardnotes/security": "workspace:^",
"axios": "^1.1.3",
"cors": "2.8.5",
"dotenv": "^16.0.1",
"express": "^4.18.2",

View File

@@ -1,7 +1,4 @@
import * as winston from 'winston'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const axios = require('axios')
import { AxiosInstance } from 'axios'
import Redis from 'ioredis'
import { SQSClient, SQSClientConfig } from '@aws-sdk/client-sqs'
import { ApiGatewayManagementApiClient } from '@aws-sdk/client-apigatewaymanagementapi'
@@ -123,7 +120,6 @@ export class ContainerConfigLoader {
.to(WebSocketMessageRequestedEventHandler)
// Services
container.bind<AxiosInstance>(TYPES.HTTPClient).toConstantValue(axios.create())
container
.bind<TokenDecoderInterface<CrossServiceTokenData>>(TYPES.CrossServiceTokenDecoder)
.toConstantValue(new TokenDecoder<CrossServiceTokenData>(container.get(TYPES.AUTH_JWT_SECRET)))

View File

@@ -29,7 +29,6 @@ const TYPES = {
WebSocketConnectionTokenEncoder: Symbol.for('WebSocketConnectionTokenEncoder'),
DomainEventSubscriber: Symbol.for('DomainEventSubscriber'),
DomainEventMessageHandler: Symbol.for('DomainEventMessageHandler'),
HTTPClient: Symbol.for('HTTPClient'),
WebSocketsClientMessenger: Symbol.for('WebSocketsClientMessenger'),
}

View File

@@ -16,11 +16,23 @@ describe('AddWebSocketsConnection', () => {
logger = {} as jest.Mocked<Logger>
logger.debug = jest.fn()
logger.error = jest.fn()
})
it('should save a web sockets connection for a user for further communication', async () => {
await createUseCase().execute({ userUuid: '1-2-3', connectionId: '2-3-4' })
const result = await createUseCase().execute({ userUuid: '1-2-3', connectionId: '2-3-4' })
expect(webSocketsConnectionRepository.saveConnection).toHaveBeenCalledWith('1-2-3', '2-3-4')
expect(result.isFailed()).toBe(false)
})
it('should return a failure if the web sockets connection could not be saved', async () => {
webSocketsConnectionRepository.saveConnection = jest
.fn()
.mockRejectedValueOnce(new Error('Could not save connection'))
const result = await createUseCase().execute({ userUuid: '1-2-3', connectionId: '2-3-4' })
expect(result.isFailed()).toBe(true)
})
})

View File

@@ -1,26 +1,32 @@
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
import TYPES from '../../../Bootstrap/Types'
import { WebSocketsConnectionRepositoryInterface } from '../../WebSockets/WebSocketsConnectionRepositoryInterface'
import { UseCaseInterface } from '../UseCaseInterface'
import { AddWebSocketsConnectionDTO } from './AddWebSocketsConnectionDTO'
import { AddWebSocketsConnectionResponse } from './AddWebSocketsConnectionResponse'
@injectable()
export class AddWebSocketsConnection implements UseCaseInterface {
export class AddWebSocketsConnection implements UseCaseInterface<void> {
constructor(
@inject(TYPES.WebSocketsConnectionRepository)
private webSocketsConnectionRepository: WebSocketsConnectionRepositoryInterface,
@inject(TYPES.Logger) private logger: Logger,
) {}
async execute(dto: AddWebSocketsConnectionDTO): Promise<AddWebSocketsConnectionResponse> {
this.logger.debug(`Persisting connection ${dto.connectionId} for user ${dto.userUuid}`)
async execute(dto: AddWebSocketsConnectionDTO): Promise<Result<void>> {
try {
this.logger.debug(`Persisting connection ${dto.connectionId} for user ${dto.userUuid}`)
await this.webSocketsConnectionRepository.saveConnection(dto.userUuid, dto.connectionId)
await this.webSocketsConnectionRepository.saveConnection(dto.userUuid, dto.connectionId)
return {
success: true,
return Result.ok()
} catch (error) {
this.logger.error(
`Error persisting connection ${dto.connectionId} for user ${dto.userUuid}: ${(error as Error).message}`,
)
return Result.fail((error as Error).message)
}
}
}

View File

@@ -1,3 +0,0 @@
export type AddWebSocketsConnectionResponse = {
success: boolean
}

View File

@@ -16,11 +16,23 @@ describe('RemoveWebSocketsConnection', () => {
logger = {} as jest.Mocked<Logger>
logger.debug = jest.fn()
logger.error = jest.fn()
})
it('should remove a web sockets connection', async () => {
await createUseCase().execute({ connectionId: '2-3-4' })
const result = await createUseCase().execute({ connectionId: '2-3-4' })
expect(webSocketsConnectionRepository.removeConnection).toHaveBeenCalledWith('2-3-4')
expect(result.isFailed()).toBe(false)
})
it('should return a failure if the web sockets connection could not be removed', async () => {
webSocketsConnectionRepository.removeConnection = jest
.fn()
.mockRejectedValueOnce(new Error('Could not remove connection'))
const result = await createUseCase().execute({ connectionId: '2-3-4' })
expect(result.isFailed()).toBe(true)
})
})

View File

@@ -1,26 +1,30 @@
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
import TYPES from '../../../Bootstrap/Types'
import { WebSocketsConnectionRepositoryInterface } from '../../WebSockets/WebSocketsConnectionRepositoryInterface'
import { UseCaseInterface } from '../UseCaseInterface'
import { RemoveWebSocketsConnectionDTO } from './RemoveWebSocketsConnectionDTO'
import { RemoveWebSocketsConnectionResponse } from './RemoveWebSocketsConnectionResponse'
@injectable()
export class RemoveWebSocketsConnection implements UseCaseInterface {
export class RemoveWebSocketsConnection implements UseCaseInterface<void> {
constructor(
@inject(TYPES.WebSocketsConnectionRepository)
private webSocketsConnectionRepository: WebSocketsConnectionRepositoryInterface,
@inject(TYPES.Logger) private logger: Logger,
) {}
async execute(dto: RemoveWebSocketsConnectionDTO): Promise<RemoveWebSocketsConnectionResponse> {
this.logger.debug(`Removing connection ${dto.connectionId}`)
async execute(dto: RemoveWebSocketsConnectionDTO): Promise<Result<void>> {
try {
this.logger.debug(`Removing connection ${dto.connectionId}`)
await this.webSocketsConnectionRepository.removeConnection(dto.connectionId)
await this.webSocketsConnectionRepository.removeConnection(dto.connectionId)
return {
success: true,
return Result.ok()
} catch (error) {
this.logger.error(`Error removing connection ${dto.connectionId}: ${(error as Error).message}`)
return Result.fail((error as Error).message)
}
}
}

View File

@@ -1,3 +0,0 @@
export type RemoveWebSocketsConnectionResponse = {
success: boolean
}

View File

@@ -36,21 +36,27 @@ export class AnnotatedWebSocketsController extends BaseHttpController {
async storeWebSocketsConnection(
request: Request,
response: Response,
): Promise<results.JsonResult | results.BadRequestErrorMessageResult> {
await this.addWebSocketsConnection.execute({
): Promise<results.OkResult | results.BadRequestResult> {
const result = await this.addWebSocketsConnection.execute({
userUuid: response.locals.user.uuid,
connectionId: request.params.connectionId,
})
return this.json({ success: true })
if (result.isFailed()) {
return this.badRequest()
}
return this.ok()
}
@httpDelete('/connections/:connectionId')
async deleteWebSocketsConnection(
request: Request,
): Promise<results.JsonResult | results.BadRequestErrorMessageResult> {
await this.removeWebSocketsConnection.execute({ connectionId: request.params.connectionId })
async deleteWebSocketsConnection(request: Request): Promise<results.OkResult | results.BadRequestResult> {
const result = await this.removeWebSocketsConnection.execute({ connectionId: request.params.connectionId })
return this.json({ success: true })
if (result.isFailed()) {
return this.badRequest()
}
return this.ok()
}
}

View File

@@ -5799,7 +5799,6 @@ __metadata:
"@types/uuid": "npm:^9.0.3"
"@typescript-eslint/eslint-plugin": "npm:^6.5.0"
"@typescript-eslint/parser": "npm:^6.5.0"
axios: "npm:^1.1.3"
cors: "npm:2.8.5"
dotenv: "npm:^16.0.1"
eslint: "npm:^8.39.0"
@@ -5877,7 +5876,6 @@ __metadata:
"@types/jest": "npm:^29.5.1"
"@typescript-eslint/eslint-plugin": "npm:^6.5.0"
"@typescript-eslint/parser": "npm:^6.5.0"
axios: "npm:^1.1.3"
cors: "npm:2.8.5"
dotenv: "npm:^16.0.1"
eslint: "npm:^8.39.0"