mirror of
https://github.com/standardnotes/server
synced 2026-01-22 02:04:30 -05:00
Compare commits
6 Commits
@standardn
...
@standardn
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d2342f9ee | ||
|
|
60838a1b7e | ||
|
|
63401b7640 | ||
|
|
6a5b669ec4 | ||
|
|
ca201447d2 | ||
|
|
f1d3117518 |
1
.pnp.cjs
generated
1
.pnp.cjs
generated
@@ -3316,6 +3316,7 @@ const RAW_RUNTIME_STATE =
|
||||
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
|
||||
["@sentry/node", "npm:7.28.1"],\
|
||||
["@sentry/tracing", "npm:7.28.1"],\
|
||||
["@standardnotes/api", "npm:1.24.9"],\
|
||||
["@standardnotes/common", "workspace:packages/common"],\
|
||||
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
|
||||
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
||||
|
||||
@@ -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.19.13](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.19.12...@standardnotes/analytics@2.19.13) (2023-01-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.19.12](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.19.11...@standardnotes/analytics@2.19.12) (2023-01-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/analytics",
|
||||
"version": "2.19.12",
|
||||
"version": "2.19.13",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <19.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,22 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.87.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.87.0...@standardnotes/auth-server@1.87.1) (2023-01-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
# [1.87.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.86.4...@standardnotes/auth-server@1.87.0) (2023-01-24)
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** add U2F to MFA verification ([6a5b669](https://github.com/standardnotes/server/commit/6a5b669ec47d3fd71fec3e362d66480d91c544d0))
|
||||
|
||||
## [1.86.4](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.86.3...@standardnotes/auth-server@1.86.4) (2023-01-24)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** add cleanup of authenticator devices upon sign in with recovery codes ([f1d3117](https://github.com/standardnotes/server/commit/f1d311751832a2abdbe124cede7f020b28cbcd9d))
|
||||
|
||||
## [1.86.3](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.86.2...@standardnotes/auth-server@1.86.3) (2023-01-24)
|
||||
|
||||
### Reverts
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/auth-server",
|
||||
"version": "1.86.3",
|
||||
"version": "1.87.1",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <19.0.0"
|
||||
},
|
||||
|
||||
@@ -665,6 +665,7 @@ export class ContainerConfigLoader {
|
||||
container.get(TYPES.IncreaseLoginAttempts),
|
||||
container.get(TYPES.ClearLoginAttempts),
|
||||
container.get(TYPES.DeleteSetting),
|
||||
container.get(TYPES.AuthenticatorRepository),
|
||||
),
|
||||
)
|
||||
container.bind<DeleteAccount>(TYPES.DeleteAccount).to(DeleteAccount)
|
||||
|
||||
@@ -4,7 +4,7 @@ import { BaseHttpController, controller, httpPost, results } from 'inversify-exp
|
||||
import { Request, Response } from 'express'
|
||||
import TYPES from '../Bootstrap/Types'
|
||||
import { CreateListedAccount } from '../Domain/UseCase/CreateListedAccount/CreateListedAccount'
|
||||
import { ErrorTag } from '@standardnotes/common'
|
||||
import { ErrorTag } from '@standardnotes/api'
|
||||
|
||||
@controller('/listed')
|
||||
export class ListedController extends BaseHttpController {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ErrorTag } from '@standardnotes/common'
|
||||
import { ErrorTag } from '@standardnotes/api'
|
||||
import { Request, Response } from 'express'
|
||||
import { inject } from 'inversify'
|
||||
import {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ErrorTag } from '@standardnotes/common'
|
||||
import { ErrorTag } from '@standardnotes/api'
|
||||
import { Request, Response } from 'express'
|
||||
import { inject } from 'inversify'
|
||||
import {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CrossServiceTokenData, TokenEncoderInterface } from '@standardnotes/security'
|
||||
import { ErrorTag } from '@standardnotes/common'
|
||||
import { ErrorTag } from '@standardnotes/api'
|
||||
import { SettingName } from '@standardnotes/settings'
|
||||
import { Request, Response } from 'express'
|
||||
import { inject } from 'inversify'
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Request, Response } from 'express'
|
||||
import { inject } from 'inversify'
|
||||
import { ErrorTag } from '@standardnotes/api'
|
||||
import {
|
||||
BaseHttpController,
|
||||
controller,
|
||||
@@ -18,7 +19,6 @@ import { GetUserSubscription } from '../Domain/UseCase/GetUserSubscription/GetUs
|
||||
import { ClearLoginAttempts } from '../Domain/UseCase/ClearLoginAttempts'
|
||||
import { IncreaseLoginAttempts } from '../Domain/UseCase/IncreaseLoginAttempts'
|
||||
import { ChangeCredentials } from '../Domain/UseCase/ChangeCredentials/ChangeCredentials'
|
||||
import { ErrorTag } from '@standardnotes/common'
|
||||
|
||||
@controller('/users')
|
||||
export class UsersController extends BaseHttpController {
|
||||
|
||||
@@ -8,12 +8,12 @@ import {
|
||||
results,
|
||||
} from 'inversify-express-utils'
|
||||
import { CreateValetTokenPayload } from '@standardnotes/responses'
|
||||
import { ErrorTag } from '@standardnotes/api'
|
||||
import { ValetTokenOperation } from '@standardnotes/security'
|
||||
import { Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
import TYPES from '../Bootstrap/Types'
|
||||
import { CreateValetToken } from '../Domain/UseCase/CreateValetToken/CreateValetToken'
|
||||
import { ErrorTag } from '@standardnotes/common'
|
||||
import { ValetTokenOperation } from '@standardnotes/security'
|
||||
import { Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
@controller('/valet-tokens', TYPES.ApiGatewayAuthMiddleware)
|
||||
export class ValetTokenController extends BaseHttpController {
|
||||
|
||||
@@ -8,4 +8,5 @@ export interface AuthenticatorRepositoryInterface {
|
||||
findByUserUuidAndCredentialId(userUuid: Uuid, credentialId: Buffer): Promise<Authenticator | null>
|
||||
save(authenticator: Authenticator): Promise<void>
|
||||
remove(authenticator: Authenticator): Promise<void>
|
||||
removeByUserUuid(userUuid: Uuid): Promise<void>
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Result } from '@standardnotes/domain-core'
|
||||
|
||||
import { AuthResponse20200115 } from '../../Auth/AuthResponse20200115'
|
||||
import { AuthResponseFactory20200115 } from '../../Auth/AuthResponseFactory20200115'
|
||||
import { AuthenticatorRepositoryInterface } from '../../Authenticator/AuthenticatorRepositoryInterface'
|
||||
import { CrypterInterface } from '../../Encryption/CrypterInterface'
|
||||
import { Setting } from '../../Setting/Setting'
|
||||
import { SettingServiceInterface } from '../../Setting/SettingServiceInterface'
|
||||
@@ -24,6 +25,7 @@ describe('SignInWithRecoveryCodes', () => {
|
||||
let increaseLoginAttempts: IncreaseLoginAttempts
|
||||
let clearLoginAttempts: ClearLoginAttempts
|
||||
let deleteSetting: DeleteSetting
|
||||
let authenticatorRepository: AuthenticatorRepositoryInterface
|
||||
|
||||
const createUseCase = () =>
|
||||
new SignInWithRecoveryCodes(
|
||||
@@ -36,12 +38,13 @@ describe('SignInWithRecoveryCodes', () => {
|
||||
increaseLoginAttempts,
|
||||
clearLoginAttempts,
|
||||
deleteSetting,
|
||||
authenticatorRepository,
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
userRepository = {} as jest.Mocked<UserRepositoryInterface>
|
||||
userRepository.findOneByEmail = jest.fn().mockReturnValue({
|
||||
uuid: '1-2-3',
|
||||
uuid: '00000000-0000-0000-0000-000000000000',
|
||||
encryptedPassword: '$2a$11$K3g6XoTau8VmLJcai1bB0eD9/YvBSBRtBhMprJOaVZ0U3SgasZH3a',
|
||||
} as jest.Mocked<User>)
|
||||
|
||||
@@ -69,6 +72,9 @@ describe('SignInWithRecoveryCodes', () => {
|
||||
|
||||
deleteSetting = {} as jest.Mocked<DeleteSetting>
|
||||
deleteSetting.execute = jest.fn()
|
||||
|
||||
authenticatorRepository = {} as jest.Mocked<AuthenticatorRepositoryInterface>
|
||||
authenticatorRepository.removeByUserUuid = jest.fn()
|
||||
})
|
||||
|
||||
it('should return error if password is not provided', async () => {
|
||||
@@ -209,6 +215,24 @@ describe('SignInWithRecoveryCodes', () => {
|
||||
expect(result.getError()).toBe('Could not sign in with recovery codes: Oops')
|
||||
})
|
||||
|
||||
it('should return error if user has an invalid uuid', async () => {
|
||||
userRepository.findOneByEmail = jest.fn().mockReturnValue({
|
||||
uuid: '1-2-3',
|
||||
encryptedPassword: '$2a$11$K3g6XoTau8VmLJcai1bB0eD9/YvBSBRtBhMprJOaVZ0U3SgasZH3a',
|
||||
} as jest.Mocked<User>)
|
||||
|
||||
const result = await createUseCase().execute({
|
||||
userAgent: 'user-agent',
|
||||
username: 'test@test.te',
|
||||
password: 'qweqwe123123',
|
||||
codeVerifier: 'code-verifier',
|
||||
recoveryCodes: 'foo',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
expect(result.getError()).toBe('Invalid user uuid')
|
||||
})
|
||||
|
||||
it('should return auth response', async () => {
|
||||
const result = await createUseCase().execute({
|
||||
userAgent: 'user-agent',
|
||||
@@ -220,6 +244,7 @@ describe('SignInWithRecoveryCodes', () => {
|
||||
|
||||
expect(clearLoginAttempts.execute).toHaveBeenCalled()
|
||||
expect(deleteSetting.execute).toHaveBeenCalled()
|
||||
expect(authenticatorRepository.removeByUserUuid).toHaveBeenCalled()
|
||||
expect(result.isFailed()).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as bcrypt from 'bcryptjs'
|
||||
import { Result, UseCaseInterface, Username, Validator } from '@standardnotes/domain-core'
|
||||
import { Result, UseCaseInterface, Username, Uuid, Validator } from '@standardnotes/domain-core'
|
||||
import { SettingName } from '@standardnotes/settings'
|
||||
import { ApiVersion } from '@standardnotes/api'
|
||||
|
||||
@@ -15,6 +15,7 @@ import { AuthResponseFactory20200115 } from '../../Auth/AuthResponseFactory20200
|
||||
import { IncreaseLoginAttempts } from '../IncreaseLoginAttempts'
|
||||
import { ClearLoginAttempts } from '../ClearLoginAttempts'
|
||||
import { DeleteSetting } from '../DeleteSetting/DeleteSetting'
|
||||
import { AuthenticatorRepositoryInterface } from '../../Authenticator/AuthenticatorRepositoryInterface'
|
||||
|
||||
export class SignInWithRecoveryCodes implements UseCaseInterface<AuthResponse20200115> {
|
||||
constructor(
|
||||
@@ -27,6 +28,7 @@ export class SignInWithRecoveryCodes implements UseCaseInterface<AuthResponse202
|
||||
private increaseLoginAttempts: IncreaseLoginAttempts,
|
||||
private clearLoginAttempts: ClearLoginAttempts,
|
||||
private deleteSetting: DeleteSetting,
|
||||
private authenticatorRepository: AuthenticatorRepositoryInterface,
|
||||
) {}
|
||||
|
||||
async execute(dto: SignInWithRecoveryCodesDTO): Promise<Result<AuthResponse20200115>> {
|
||||
@@ -65,6 +67,14 @@ export class SignInWithRecoveryCodes implements UseCaseInterface<AuthResponse202
|
||||
return Result.fail('Could not find user')
|
||||
}
|
||||
|
||||
const userUuidOrError = Uuid.create(user.uuid)
|
||||
if (userUuidOrError.isFailed()) {
|
||||
await this.increaseLoginAttempts.execute({ email: username.value })
|
||||
|
||||
return Result.fail('Invalid user uuid')
|
||||
}
|
||||
const userUuid = userUuidOrError.getValue()
|
||||
|
||||
const passwordMatches = await bcrypt.compare(dto.password, user.encryptedPassword)
|
||||
if (!passwordMatches) {
|
||||
await this.increaseLoginAttempts.execute({ email: username.value })
|
||||
@@ -110,6 +120,8 @@ export class SignInWithRecoveryCodes implements UseCaseInterface<AuthResponse202
|
||||
userUuid: user.uuid,
|
||||
})
|
||||
|
||||
await this.authenticatorRepository.removeByUserUuid(userUuid)
|
||||
|
||||
await this.clearLoginAttempts.execute({ email: username.value })
|
||||
|
||||
return Result.ok(authResponse as AuthResponse20200115)
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import 'reflect-metadata'
|
||||
import { authenticator } from 'otplib'
|
||||
import { SettingName } from '@standardnotes/settings'
|
||||
import { SelectorInterface } from '@standardnotes/security'
|
||||
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
|
||||
|
||||
import { User } from '../User/User'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
import { VerifyMFA } from './VerifyMFA'
|
||||
import { Setting } from '../Setting/Setting'
|
||||
import { SettingServiceInterface } from '../Setting/SettingServiceInterface'
|
||||
import { SettingName } from '@standardnotes/settings'
|
||||
import { SelectorInterface } from '@standardnotes/security'
|
||||
import { LockRepositoryInterface } from '../User/LockRepositoryInterface'
|
||||
import { AuthenticatorRepositoryInterface } from '../Authenticator/AuthenticatorRepositoryInterface'
|
||||
|
||||
import { VerifyMFA } from './VerifyMFA'
|
||||
import { Logger } from 'winston'
|
||||
import { Authenticator } from '../Authenticator/Authenticator'
|
||||
|
||||
describe('VerifyMFA', () => {
|
||||
let user: User
|
||||
@@ -17,13 +22,27 @@ describe('VerifyMFA', () => {
|
||||
let settingService: SettingServiceInterface
|
||||
let booleanSelector: SelectorInterface<boolean>
|
||||
let lockRepository: LockRepositoryInterface
|
||||
let authenticatorRepository: AuthenticatorRepositoryInterface
|
||||
let verifyAuthenticatorAuthenticationResponse: UseCaseInterface<boolean>
|
||||
let logger: Logger
|
||||
const pseudoKeyParamsKey = 'foobar'
|
||||
|
||||
const createVerifyMFA = () =>
|
||||
new VerifyMFA(userRepository, settingService, booleanSelector, lockRepository, pseudoKeyParamsKey)
|
||||
new VerifyMFA(
|
||||
userRepository,
|
||||
settingService,
|
||||
booleanSelector,
|
||||
lockRepository,
|
||||
pseudoKeyParamsKey,
|
||||
authenticatorRepository,
|
||||
verifyAuthenticatorAuthenticationResponse,
|
||||
logger,
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
user = {} as jest.Mocked<User>
|
||||
user = {
|
||||
uuid: '00000000-0000-0000-0000-000000000000',
|
||||
} as jest.Mocked<User>
|
||||
|
||||
userRepository = {} as jest.Mocked<UserRepositoryInterface>
|
||||
userRepository.findOneByEmail = jest.fn().mockReturnValue(user)
|
||||
@@ -42,164 +61,270 @@ describe('VerifyMFA', () => {
|
||||
|
||||
settingService = {} as jest.Mocked<SettingServiceInterface>
|
||||
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(setting)
|
||||
|
||||
authenticatorRepository = {} as jest.Mocked<AuthenticatorRepositoryInterface>
|
||||
authenticatorRepository.findByUserUuid = jest.fn().mockReturnValue([])
|
||||
|
||||
verifyAuthenticatorAuthenticationResponse = {} as jest.Mocked<UseCaseInterface<boolean>>
|
||||
verifyAuthenticatorAuthenticationResponse.execute = jest.fn().mockReturnValue(Result.ok())
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.debug = jest.fn()
|
||||
})
|
||||
|
||||
it('should pass MFA verification if user has no MFA enabled', async () => {
|
||||
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(null)
|
||||
describe('2FA', () => {
|
||||
it('should pass MFA verification if user has no MFA enabled', async () => {
|
||||
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(null)
|
||||
|
||||
expect(
|
||||
await createVerifyMFA().execute({ email: 'test@test.te', requestParams: {}, preventOTPFromFurtherUsage: true }),
|
||||
).toEqual({
|
||||
success: true,
|
||||
})
|
||||
|
||||
expect(lockRepository.lockSuccessfullOTP).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should pass MFA verification if user has MFA deleted', async () => {
|
||||
setting = {
|
||||
name: SettingName.MfaSecret,
|
||||
value: null,
|
||||
} as jest.Mocked<Setting>
|
||||
|
||||
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(setting)
|
||||
|
||||
expect(
|
||||
await createVerifyMFA().execute({ email: 'test@test.te', requestParams: {}, preventOTPFromFurtherUsage: true }),
|
||||
).toEqual({
|
||||
success: true,
|
||||
})
|
||||
|
||||
expect(lockRepository.lockSuccessfullOTP).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should pass MFA verification if user is not found and pseudo mfa is not required', async () => {
|
||||
userRepository.findOneByEmail = jest.fn().mockReturnValue(null)
|
||||
expect(
|
||||
await createVerifyMFA().execute({ email: 'test@test.te', requestParams: {}, preventOTPFromFurtherUsage: true }),
|
||||
).toEqual({
|
||||
success: true,
|
||||
})
|
||||
|
||||
expect(lockRepository.lockSuccessfullOTP).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not pass MFA verification if user is not found and pseudo mfa is required', async () => {
|
||||
booleanSelector.select = jest.fn().mockReturnValue(true)
|
||||
userRepository.findOneByEmail = jest.fn().mockReturnValue(null)
|
||||
|
||||
expect(
|
||||
await createVerifyMFA().execute({ email: 'test@test.te', requestParams: {}, preventOTPFromFurtherUsage: true }),
|
||||
).toEqual({
|
||||
success: false,
|
||||
errorTag: 'mfa-required',
|
||||
errorMessage: 'Please enter your two-factor authentication code.',
|
||||
errorPayload: { mfa_key: expect.stringMatching(/^mfa_/) },
|
||||
})
|
||||
})
|
||||
|
||||
it('should pass MFA verification if mfa key is correctly encrypted', async () => {
|
||||
expect(
|
||||
await createVerifyMFA().execute({
|
||||
email: 'test@test.te',
|
||||
requestParams: { 'mfa_1-2-3': authenticator.generate('shhhh') },
|
||||
preventOTPFromFurtherUsage: true,
|
||||
}),
|
||||
).toEqual({
|
||||
success: true,
|
||||
})
|
||||
|
||||
expect(lockRepository.lockSuccessfullOTP).toHaveBeenCalledWith('test@test.te', expect.any(String))
|
||||
})
|
||||
|
||||
it('should pass MFA verification without locking otp', async () => {
|
||||
expect(
|
||||
await createVerifyMFA().execute({
|
||||
email: 'test@test.te',
|
||||
requestParams: { 'mfa_1-2-3': authenticator.generate('shhhh') },
|
||||
preventOTPFromFurtherUsage: false,
|
||||
}),
|
||||
).toEqual({
|
||||
success: true,
|
||||
})
|
||||
|
||||
expect(lockRepository.lockSuccessfullOTP).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not pass MFA verification if otp is already used within lock out period', async () => {
|
||||
lockRepository.isOTPLocked = jest.fn().mockReturnValue(true)
|
||||
|
||||
expect(
|
||||
await createVerifyMFA().execute({
|
||||
email: 'test@test.te',
|
||||
requestParams: { 'mfa_1-2-3': authenticator.generate('shhhh') },
|
||||
preventOTPFromFurtherUsage: true,
|
||||
}),
|
||||
).toEqual({
|
||||
success: false,
|
||||
errorTag: 'mfa-invalid',
|
||||
errorMessage:
|
||||
'The two-factor authentication code you entered has been already utilized. Please try again in a while.',
|
||||
errorPayload: { mfa_key: 'mfa_1-2-3' },
|
||||
})
|
||||
|
||||
expect(lockRepository.lockSuccessfullOTP).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not pass MFA verification if mfa is not correct', async () => {
|
||||
setting = {
|
||||
name: SettingName.MfaSecret,
|
||||
value: 'shhhh2',
|
||||
} as jest.Mocked<Setting>
|
||||
|
||||
settingService = {} as jest.Mocked<SettingServiceInterface>
|
||||
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(setting)
|
||||
|
||||
expect(
|
||||
await createVerifyMFA().execute({
|
||||
email: 'test@test.te',
|
||||
requestParams: { 'mfa_1-2-3': 'test' },
|
||||
preventOTPFromFurtherUsage: true,
|
||||
}),
|
||||
).toEqual({
|
||||
success: false,
|
||||
errorTag: 'mfa-invalid',
|
||||
errorMessage: 'The two-factor authentication code you entered is incorrect. Please try again.',
|
||||
errorPayload: { mfa_key: 'mfa_1-2-3' },
|
||||
})
|
||||
})
|
||||
|
||||
it('should not pass MFA verification if no mfa param is found in the request', async () => {
|
||||
expect(
|
||||
await createVerifyMFA().execute({
|
||||
email: 'test@test.te',
|
||||
requestParams: { foo: 'bar' },
|
||||
preventOTPFromFurtherUsage: true,
|
||||
}),
|
||||
).toEqual({
|
||||
success: false,
|
||||
errorTag: 'mfa-required',
|
||||
errorMessage: 'Please enter your two-factor authentication code.',
|
||||
errorPayload: { mfa_key: expect.stringMatching(/^mfa_/) },
|
||||
})
|
||||
})
|
||||
|
||||
it('should throw an error if the error is not handled mfa validation error', async () => {
|
||||
settingService.findSettingWithDecryptedValue = jest.fn().mockImplementation(() => {
|
||||
throw new Error('oops!')
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await createVerifyMFA().execute({
|
||||
email: 'test@test.te',
|
||||
requestParams: { 'mfa_1-2-3': 'test' },
|
||||
preventOTPFromFurtherUsage: true,
|
||||
expect(
|
||||
await createVerifyMFA().execute({ email: 'test@test.te', requestParams: {}, preventOTPFromFurtherUsage: true }),
|
||||
).toEqual({
|
||||
success: true,
|
||||
})
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
expect(lockRepository.lockSuccessfullOTP).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should pass MFA verification if user has MFA deleted', async () => {
|
||||
setting = {
|
||||
name: SettingName.MfaSecret,
|
||||
value: null,
|
||||
} as jest.Mocked<Setting>
|
||||
|
||||
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(setting)
|
||||
|
||||
expect(
|
||||
await createVerifyMFA().execute({ email: 'test@test.te', requestParams: {}, preventOTPFromFurtherUsage: true }),
|
||||
).toEqual({
|
||||
success: true,
|
||||
})
|
||||
|
||||
expect(lockRepository.lockSuccessfullOTP).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should pass MFA verification if user is not found and pseudo mfa is not required', async () => {
|
||||
userRepository.findOneByEmail = jest.fn().mockReturnValue(null)
|
||||
expect(
|
||||
await createVerifyMFA().execute({ email: 'test@test.te', requestParams: {}, preventOTPFromFurtherUsage: true }),
|
||||
).toEqual({
|
||||
success: true,
|
||||
})
|
||||
|
||||
expect(lockRepository.lockSuccessfullOTP).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not pass MFA verification if user is not found and pseudo mfa is required', async () => {
|
||||
booleanSelector.select = jest.fn().mockReturnValue(true)
|
||||
userRepository.findOneByEmail = jest.fn().mockReturnValue(null)
|
||||
|
||||
expect(
|
||||
await createVerifyMFA().execute({ email: 'test@test.te', requestParams: {}, preventOTPFromFurtherUsage: true }),
|
||||
).toEqual({
|
||||
success: false,
|
||||
errorTag: 'mfa-required',
|
||||
errorMessage: 'Please enter your two-factor authentication code.',
|
||||
errorPayload: { mfa_key: expect.stringMatching(/^mfa_/) },
|
||||
})
|
||||
})
|
||||
|
||||
it('should pass MFA verification if mfa key is correctly encrypted', async () => {
|
||||
expect(
|
||||
await createVerifyMFA().execute({
|
||||
email: 'test@test.te',
|
||||
requestParams: { 'mfa_1-2-3': authenticator.generate('shhhh') },
|
||||
preventOTPFromFurtherUsage: true,
|
||||
}),
|
||||
).toEqual({
|
||||
success: true,
|
||||
})
|
||||
|
||||
expect(lockRepository.lockSuccessfullOTP).toHaveBeenCalledWith('test@test.te', expect.any(String))
|
||||
})
|
||||
|
||||
it('should pass MFA verification without locking otp', async () => {
|
||||
expect(
|
||||
await createVerifyMFA().execute({
|
||||
email: 'test@test.te',
|
||||
requestParams: { 'mfa_1-2-3': authenticator.generate('shhhh') },
|
||||
preventOTPFromFurtherUsage: false,
|
||||
}),
|
||||
).toEqual({
|
||||
success: true,
|
||||
})
|
||||
|
||||
expect(lockRepository.lockSuccessfullOTP).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not pass MFA verification if otp is already used within lock out period', async () => {
|
||||
lockRepository.isOTPLocked = jest.fn().mockReturnValue(true)
|
||||
|
||||
expect(
|
||||
await createVerifyMFA().execute({
|
||||
email: 'test@test.te',
|
||||
requestParams: { 'mfa_1-2-3': authenticator.generate('shhhh') },
|
||||
preventOTPFromFurtherUsage: true,
|
||||
}),
|
||||
).toEqual({
|
||||
success: false,
|
||||
errorTag: 'mfa-invalid',
|
||||
errorMessage:
|
||||
'The two-factor authentication code you entered has been already utilized. Please try again in a while.',
|
||||
errorPayload: { mfa_key: 'mfa_1-2-3' },
|
||||
})
|
||||
|
||||
expect(lockRepository.lockSuccessfullOTP).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not pass MFA verification if mfa is not correct', async () => {
|
||||
setting = {
|
||||
name: SettingName.MfaSecret,
|
||||
value: 'shhhh2',
|
||||
} as jest.Mocked<Setting>
|
||||
|
||||
settingService = {} as jest.Mocked<SettingServiceInterface>
|
||||
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(setting)
|
||||
|
||||
expect(
|
||||
await createVerifyMFA().execute({
|
||||
email: 'test@test.te',
|
||||
requestParams: { 'mfa_1-2-3': 'test' },
|
||||
preventOTPFromFurtherUsage: true,
|
||||
}),
|
||||
).toEqual({
|
||||
success: false,
|
||||
errorTag: 'mfa-invalid',
|
||||
errorMessage: 'The two-factor authentication code you entered is incorrect. Please try again.',
|
||||
errorPayload: { mfa_key: 'mfa_1-2-3' },
|
||||
})
|
||||
})
|
||||
|
||||
it('should not pass MFA verification if no mfa param is found in the request', async () => {
|
||||
expect(
|
||||
await createVerifyMFA().execute({
|
||||
email: 'test@test.te',
|
||||
requestParams: { foo: 'bar' },
|
||||
preventOTPFromFurtherUsage: true,
|
||||
}),
|
||||
).toEqual({
|
||||
success: false,
|
||||
errorTag: 'mfa-required',
|
||||
errorMessage: 'Please enter your two-factor authentication code.',
|
||||
errorPayload: { mfa_key: expect.stringMatching(/^mfa_/) },
|
||||
})
|
||||
})
|
||||
|
||||
it('should throw an error if the error is not handled mfa validation error', async () => {
|
||||
settingService.findSettingWithDecryptedValue = jest.fn().mockImplementation(() => {
|
||||
throw new Error('oops!')
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await createVerifyMFA().execute({
|
||||
email: 'test@test.te',
|
||||
requestParams: { 'mfa_1-2-3': 'test' },
|
||||
preventOTPFromFurtherUsage: true,
|
||||
})
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('U2F', () => {
|
||||
beforeEach(() => {
|
||||
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(null)
|
||||
|
||||
authenticatorRepository.findByUserUuid = jest.fn().mockReturnValue([{} as jest.Mocked<Authenticator>])
|
||||
})
|
||||
|
||||
it('should not pass if the user has an invalid uuid', async () => {
|
||||
userRepository.findOneByEmail = jest.fn().mockReturnValue({ uuid: 'invalid' } as jest.Mocked<User>)
|
||||
|
||||
expect(
|
||||
await createVerifyMFA().execute({
|
||||
email: 'test@test.te',
|
||||
requestParams: {},
|
||||
preventOTPFromFurtherUsage: true,
|
||||
}),
|
||||
).toEqual({
|
||||
success: false,
|
||||
errorMessage: 'User UUID is invalid.',
|
||||
})
|
||||
})
|
||||
|
||||
it('should not pass if the request is missing authenticator response', async () => {
|
||||
expect(
|
||||
await createVerifyMFA().execute({
|
||||
email: 'test@test.te',
|
||||
requestParams: {},
|
||||
preventOTPFromFurtherUsage: true,
|
||||
}),
|
||||
).toEqual({
|
||||
success: false,
|
||||
errorTag: 'mfa-required',
|
||||
errorMessage: 'Please authenticate with your U2F device.',
|
||||
})
|
||||
})
|
||||
|
||||
it('should not pass if the authenticator response verification fails', async () => {
|
||||
verifyAuthenticatorAuthenticationResponse.execute = jest.fn().mockReturnValue(Result.fail('oops!'))
|
||||
|
||||
expect(
|
||||
await createVerifyMFA().execute({
|
||||
email: 'test@test.te',
|
||||
requestParams: {
|
||||
authenticator_response: {
|
||||
id: Buffer.from([1]),
|
||||
},
|
||||
},
|
||||
preventOTPFromFurtherUsage: true,
|
||||
}),
|
||||
).toEqual({
|
||||
success: false,
|
||||
errorTag: 'mfa-invalid',
|
||||
errorMessage: 'Could not verify U2F device.',
|
||||
})
|
||||
})
|
||||
|
||||
it('should not pass if the authenticator is not verified', async () => {
|
||||
verifyAuthenticatorAuthenticationResponse.execute = jest.fn().mockReturnValue(Result.ok(false))
|
||||
|
||||
expect(
|
||||
await createVerifyMFA().execute({
|
||||
email: 'test@test.te',
|
||||
requestParams: {
|
||||
authenticator_response: {
|
||||
id: Buffer.from([1]),
|
||||
},
|
||||
},
|
||||
preventOTPFromFurtherUsage: true,
|
||||
}),
|
||||
).toEqual({
|
||||
success: false,
|
||||
errorTag: 'mfa-invalid',
|
||||
errorMessage: 'Could not verify U2F device.',
|
||||
})
|
||||
})
|
||||
|
||||
it('should pass if the authenticator is verified', async () => {
|
||||
verifyAuthenticatorAuthenticationResponse.execute = jest.fn().mockReturnValue(Result.ok(true))
|
||||
|
||||
expect(
|
||||
await createVerifyMFA().execute({
|
||||
email: 'test@test.te',
|
||||
requestParams: {
|
||||
authenticator_response: {
|
||||
id: Buffer.from([1]),
|
||||
},
|
||||
},
|
||||
preventOTPFromFurtherUsage: true,
|
||||
}),
|
||||
).toEqual({
|
||||
success: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
import * as crypto from 'crypto'
|
||||
import { ErrorTag } from '@standardnotes/common'
|
||||
import { ErrorTag } from '@standardnotes/api'
|
||||
import { SettingName } from '@standardnotes/settings'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { authenticator } from 'otplib'
|
||||
import { SelectorInterface } from '@standardnotes/security'
|
||||
import { UseCaseInterface as DomainUseCaseInterface, Uuid } from '@standardnotes/domain-core'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { MFAValidationError } from '../Error/MFAValidationError'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
import { SettingServiceInterface } from '../Setting/SettingServiceInterface'
|
||||
import { LockRepositoryInterface } from '../User/LockRepositoryInterface'
|
||||
import { AuthenticatorRepositoryInterface } from '../Authenticator/AuthenticatorRepositoryInterface'
|
||||
|
||||
import { UseCaseInterface } from './UseCaseInterface'
|
||||
import { VerifyMFADTO } from './VerifyMFADTO'
|
||||
import { VerifyMFAResponse } from './VerifyMFAResponse'
|
||||
import { SettingServiceInterface } from '../Setting/SettingServiceInterface'
|
||||
import { SelectorInterface } from '@standardnotes/security'
|
||||
import { LockRepositoryInterface } from '../User/LockRepositoryInterface'
|
||||
import { Logger } from 'winston'
|
||||
import { Setting } from '../Setting/Setting'
|
||||
|
||||
@injectable()
|
||||
export class VerifyMFA implements UseCaseInterface {
|
||||
@@ -23,6 +28,10 @@ export class VerifyMFA implements UseCaseInterface {
|
||||
@inject(TYPES.BooleanSelector) private booleanSelector: SelectorInterface<boolean>,
|
||||
@inject(TYPES.LockRepository) private lockRepository: LockRepositoryInterface,
|
||||
@inject(TYPES.PSEUDO_KEY_PARAMS_KEY) private pseudoKeyParamsKey: string,
|
||||
@inject(TYPES.AuthenticatorRepository) private authenticatorRepository: AuthenticatorRepositoryInterface,
|
||||
@inject(TYPES.VerifyAuthenticatorAuthenticationResponse)
|
||||
private verifyAuthenticatorAuthenticationResponse: DomainUseCaseInterface<boolean>,
|
||||
@inject(TYPES.Logger) private logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(dto: VerifyMFADTO): Promise<VerifyMFAResponse> {
|
||||
@@ -48,24 +57,78 @@ export class VerifyMFA implements UseCaseInterface {
|
||||
}
|
||||
}
|
||||
|
||||
const userUuidOrError = Uuid.create(user.uuid)
|
||||
if (userUuidOrError.isFailed()) {
|
||||
return {
|
||||
success: false,
|
||||
errorMessage: 'User UUID is invalid.',
|
||||
}
|
||||
}
|
||||
const userUuid = userUuidOrError.getValue()
|
||||
|
||||
let u2fEnabled = false
|
||||
const u2fAuthenticators = await this.authenticatorRepository.findByUserUuid(userUuid)
|
||||
if (u2fAuthenticators.length > 0) {
|
||||
u2fEnabled = true
|
||||
}
|
||||
|
||||
const mfaSecret = await this.settingService.findSettingWithDecryptedValue({
|
||||
userUuid: user.uuid,
|
||||
settingName: SettingName.MfaSecret,
|
||||
})
|
||||
if (mfaSecret === null || mfaSecret.value === null) {
|
||||
const twoFactorEnabled = mfaSecret !== null && mfaSecret.value !== null
|
||||
|
||||
if (u2fEnabled === false && twoFactorEnabled === false) {
|
||||
return {
|
||||
success: true,
|
||||
}
|
||||
}
|
||||
|
||||
const verificationResult = await this.verifyMFASecret(
|
||||
dto.email,
|
||||
mfaSecret.value,
|
||||
dto.requestParams,
|
||||
dto.preventOTPFromFurtherUsage,
|
||||
)
|
||||
if (u2fEnabled) {
|
||||
if (!dto.requestParams.authenticator_response) {
|
||||
return {
|
||||
success: false,
|
||||
errorTag: ErrorTag.MfaRequired,
|
||||
errorMessage: 'Please authenticate with your U2F device.',
|
||||
}
|
||||
}
|
||||
|
||||
return verificationResult
|
||||
const verificationResultOrError = await this.verifyAuthenticatorAuthenticationResponse.execute({
|
||||
userUuid: userUuid.value,
|
||||
authenticatorResponse: dto.requestParams.authenticator_response,
|
||||
})
|
||||
if (verificationResultOrError.isFailed()) {
|
||||
this.logger.debug(`Could not verify U2F authentication: ${verificationResultOrError.getError()}`)
|
||||
|
||||
return {
|
||||
success: false,
|
||||
errorTag: ErrorTag.MfaInvalid,
|
||||
errorMessage: 'Could not verify U2F device.',
|
||||
}
|
||||
}
|
||||
|
||||
const verificationResult = verificationResultOrError.getValue()
|
||||
if (verificationResult === false) {
|
||||
return {
|
||||
success: false,
|
||||
errorTag: ErrorTag.MfaInvalid,
|
||||
errorMessage: 'Could not verify U2F device.',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
}
|
||||
} else {
|
||||
const verificationResult = await this.verifyMFASecret(
|
||||
dto.email,
|
||||
(mfaSecret as Setting).value as string,
|
||||
dto.requestParams,
|
||||
dto.preventOTPFromFurtherUsage,
|
||||
)
|
||||
|
||||
return verificationResult
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof MFAValidationError) {
|
||||
return {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Request, Response } from 'express'
|
||||
import { ErrorTag } from '@standardnotes/api'
|
||||
import {
|
||||
BaseHttpController,
|
||||
controller,
|
||||
@@ -16,7 +17,6 @@ import { VerifyMFA } from '../../Domain/UseCase/VerifyMFA'
|
||||
import { IncreaseLoginAttempts } from '../../Domain/UseCase/IncreaseLoginAttempts'
|
||||
import { Logger } from 'winston'
|
||||
import { GetUserKeyParams } from '../../Domain/UseCase/GetUserKeyParams/GetUserKeyParams'
|
||||
import { ErrorTag } from '@standardnotes/common'
|
||||
import { inject } from 'inversify'
|
||||
import { AuthController } from '../../Controller/AuthController'
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ErrorTag } from '@standardnotes/common'
|
||||
import { ErrorTag } from '@standardnotes/api'
|
||||
import { TokenDecoderInterface, WebSocketConnectionTokenData } from '@standardnotes/security'
|
||||
import { Request } from 'express'
|
||||
import { inject } from 'inversify'
|
||||
|
||||
@@ -11,6 +11,14 @@ export class MySQLAuthenticatorRepository implements AuthenticatorRepositoryInte
|
||||
private mapper: MapperInterface<Authenticator, TypeORMAuthenticator>,
|
||||
) {}
|
||||
|
||||
async removeByUserUuid(userUuid: Uuid): Promise<void> {
|
||||
await this.ormRepository
|
||||
.createQueryBuilder()
|
||||
.delete()
|
||||
.where('user_uuid = :userUuid', { userUuid: userUuid.value })
|
||||
.execute()
|
||||
}
|
||||
|
||||
async findById(id: UniqueEntityId): Promise<Authenticator | null> {
|
||||
const persistence = await this.ormRepository
|
||||
.createQueryBuilder('authenticator')
|
||||
|
||||
@@ -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.46.5](https://github.com/standardnotes/server/compare/@standardnotes/common@1.46.4...@standardnotes/common@1.46.5) (2023-01-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/common
|
||||
|
||||
## [1.46.4](https://github.com/standardnotes/server/compare/@standardnotes/common@1.46.3...@standardnotes/common@1.46.4) (2023-01-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/common
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/common",
|
||||
"version": "1.46.4",
|
||||
"version": "1.46.5",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <19.0.0"
|
||||
},
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
/* istanbul ignore file */
|
||||
export enum ErrorTag {
|
||||
MfaInvalid = 'mfa-invalid',
|
||||
MfaRequired = 'mfa-required',
|
||||
RefreshTokenInvalid = 'invalid-refresh-token',
|
||||
RefreshTokenExpired = 'expired-refresh-token',
|
||||
AccessTokenExpired = 'expired-access-token',
|
||||
ParametersInvalid = 'invalid-parameters',
|
||||
RevokedSession = 'revoked-session',
|
||||
AuthInvalid = 'invalid-auth',
|
||||
ReadOnlyAccess = 'read-only-access',
|
||||
}
|
||||
@@ -6,7 +6,6 @@ export * from './DataType/JSONString'
|
||||
export * from './DataType/MicrosecondsTimestamp'
|
||||
export * from './DataType/ApplicationIdentifier'
|
||||
export * from './Email/EmailMessageIdentifier'
|
||||
export * from './Error/ErrorTag'
|
||||
export * from './KeyParams/AnyKeyParamsContent'
|
||||
export * from './KeyParams/BaseKeyParams'
|
||||
export * from './KeyParams/KeyParamsContent001'
|
||||
|
||||
@@ -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.10.24](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.10.23...@standardnotes/revisions-server@1.10.24) (2023-01-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/revisions-server
|
||||
|
||||
## [1.10.23](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.10.22...@standardnotes/revisions-server@1.10.23) (2023-01-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/revisions-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/revisions-server",
|
||||
"version": "1.10.23",
|
||||
"version": "1.10.24",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <19.0.0"
|
||||
},
|
||||
|
||||
@@ -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.29.10](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.29.9...@standardnotes/syncing-server@1.29.10) (2023-01-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/syncing-server
|
||||
|
||||
## [1.29.9](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.29.8...@standardnotes/syncing-server@1.29.9) (2023-01-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/syncing-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/syncing-server",
|
||||
"version": "1.29.9",
|
||||
"version": "1.29.10",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <19.0.0"
|
||||
},
|
||||
@@ -29,6 +29,7 @@
|
||||
"@newrelic/winston-enricher": "^4.0.0",
|
||||
"@sentry/node": "^7.28.1",
|
||||
"@sentry/tracing": "^7.28.1",
|
||||
"@standardnotes/api": "^1.24.9",
|
||||
"@standardnotes/common": "workspace:*",
|
||||
"@standardnotes/domain-core": "workspace:^",
|
||||
"@standardnotes/domain-events": "workspace:*",
|
||||
|
||||
@@ -6,7 +6,7 @@ import TYPES from '../Bootstrap/Types'
|
||||
import { ProjectorInterface } from '../Projection/ProjectorInterface'
|
||||
import { Revision } from '../Domain/Revision/Revision'
|
||||
import { RevisionServiceInterface } from '../Domain/Revision/RevisionServiceInterface'
|
||||
import { ErrorTag } from '@standardnotes/common'
|
||||
import { ErrorTag } from '@standardnotes/api'
|
||||
import { RevisionProjection } from '../Projection/RevisionProjection'
|
||||
import { MapperInterface } from '@standardnotes/domain-core'
|
||||
import { RevisionMetadata } from '../Domain/Revision/RevisionMetadata'
|
||||
|
||||
@@ -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.5.16](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.5.15...@standardnotes/websockets-server@1.5.16) (2023-01-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/websockets-server
|
||||
|
||||
## [1.5.15](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.5.14...@standardnotes/websockets-server@1.5.15) (2023-01-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/websockets-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/websockets-server",
|
||||
"version": "1.5.15",
|
||||
"version": "1.5.16",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <19.0.0"
|
||||
},
|
||||
|
||||
@@ -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.19.19](https://github.com/standardnotes/server/compare/@standardnotes/workspace-server@1.19.18...@standardnotes/workspace-server@1.19.19) (2023-01-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/workspace-server
|
||||
|
||||
## [1.19.18](https://github.com/standardnotes/server/compare/@standardnotes/workspace-server@1.19.17...@standardnotes/workspace-server@1.19.18) (2023-01-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/workspace-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/workspace-server",
|
||||
"version": "1.19.18",
|
||||
"version": "1.19.19",
|
||||
"engines": {
|
||||
"node": ">=18.0.0 <19.0.0"
|
||||
},
|
||||
|
||||
@@ -2509,6 +2509,7 @@ __metadata:
|
||||
"@newrelic/winston-enricher": "npm:^4.0.0"
|
||||
"@sentry/node": "npm:^7.28.1"
|
||||
"@sentry/tracing": "npm:^7.28.1"
|
||||
"@standardnotes/api": "npm:^1.24.9"
|
||||
"@standardnotes/common": "workspace:*"
|
||||
"@standardnotes/domain-core": "workspace:^"
|
||||
"@standardnotes/domain-events": "workspace:*"
|
||||
|
||||
Reference in New Issue
Block a user