Compare commits

..

14 Commits

Author SHA1 Message Date
standardci
11011fa15d chore(release): publish new version
- @standardnotes/syncing-server@1.8.7
2022-09-20 08:01:52 +00:00
Karol Sójko
c2e9f3e72b fix(syncing-server): content size calculation and add syncing upper bound for limit paramter 2022-09-20 09:59:40 +02:00
standardci
f0fb7fd1cd chore(release): publish new version
- @standardnotes/files-server@1.6.2
2022-09-19 11:55:08 +00:00
Karol Sójko
15e342fd51 Merge pull request #224 from standardnotes/fs_dos
fix: add upper bound for FS file chunk upload
2022-09-19 13:53:39 +02:00
Karol Sójko
dfa7e06f87 fix: add upper bound for FS file chunk upload 2022-09-19 13:44:37 +02:00
standardci
a9aef5521b chore(release): publish new version
- @standardnotes/auth-server@1.29.1
 - @standardnotes/files-server@1.6.1
2022-09-19 07:59:14 +00:00
Karol Sójko
a628bdc44e fix(files): uuid validator binding 2022-09-19 09:57:17 +02:00
Karol Sójko
db6f966045 fix(auth): uuid validator binding 2022-09-19 09:57:10 +02:00
standardci
9b602ed405 chore(release): publish new version
- @standardnotes/api-gateway@1.19.6
 - @standardnotes/auth-server@1.29.0
 - @standardnotes/common@1.33.0
 - @standardnotes/domain-events-infra@1.8.11
 - @standardnotes/domain-events@2.60.5
 - @standardnotes/event-store@1.3.16
 - @standardnotes/files-server@1.6.0
 - @standardnotes/predicates@1.4.2
 - @standardnotes/scheduler-server@1.10.30
 - @standardnotes/security@1.3.3
 - @standardnotes/syncing-server@1.8.6
2022-09-19 07:45:26 +00:00
Karol Sójko
db15457ce4 feat(files): add validating remote identifiers 2022-09-19 09:43:46 +02:00
standardci
719d8558a3 chore(release): publish new version
- @standardnotes/auth-server@1.28.4
2022-09-16 10:36:18 +00:00
Karol Sójko
c207c3fc84 fix(auth): feature service spec 2022-09-16 12:34:43 +02:00
standardci
4bde4758c3 chore(release): publish new version
- @standardnotes/analytics@1.29.1
 - @standardnotes/api-gateway@1.19.5
 - @standardnotes/auth-server@1.28.3
 - @standardnotes/syncing-server@1.8.5
2022-09-16 10:19:03 +00:00
Karol Sójko
5eb957c82a fix(auth): change remaining subscription time stats to percentage 2022-09-16 12:17:34 +02:00
55 changed files with 452 additions and 62 deletions

View File

@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.29.1](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.29.0...@standardnotes/analytics@1.29.1) (2022-09-16)
### Bug Fixes
* **auth:** change remaining subscription time stats to percentage ([5eb957c](https://github.com/standardnotes/server/commit/5eb957c82a8cc5fdcb6815e2cd30e49cd2b1e8ac))
# [1.29.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.28.0...@standardnotes/analytics@1.29.0) (2022-09-15)
### Features

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/analytics",
"version": "1.29.0",
"version": "1.29.1",
"engines": {
"node": ">=14.0.0 <17.0.0"
},

View File

@@ -3,7 +3,7 @@ export enum StatisticsMeasure {
SubscriptionLength = 'subscription-length',
RegistrationLength = 'registration-length',
RegistrationToSubscriptionTime = 'registration-to-subscription-time',
SubscriptionCancelToExpireTime = 'subscription-cancel-to-expire-time',
RemainingSubscriptionTimePercentage = 'remaining-subscription-time-percentage',
Refunds = 'refunds',
NotesCountFreeUsers = 'notes-count-free-users',
NotesCountPaidUsers = 'notes-count-paid-users',

View File

@@ -3,6 +3,16 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.19.6](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.19.5...@standardnotes/api-gateway@1.19.6) (2022-09-19)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.19.5](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.19.4...@standardnotes/api-gateway@1.19.5) (2022-09-16)
### Bug Fixes
* **auth:** change remaining subscription time stats to percentage ([5eb957c](https://github.com/standardnotes/api-gateway/commit/5eb957c82a8cc5fdcb6815e2cd30e49cd2b1e8ac))
## [1.19.4](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.19.3...@standardnotes/api-gateway@1.19.4) (2022-09-16)
**Note:** Version bump only for package @standardnotes/api-gateway

View File

@@ -94,7 +94,7 @@ const requestReport = async (
StatisticsMeasure.RegistrationLength,
StatisticsMeasure.SubscriptionLength,
StatisticsMeasure.RegistrationToSubscriptionTime,
StatisticsMeasure.SubscriptionCancelToExpireTime,
StatisticsMeasure.RemainingSubscriptionTimePercentage,
StatisticsMeasure.NotesCountFreeUsers,
StatisticsMeasure.NotesCountPaidUsers,
StatisticsMeasure.FilesCount,

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/api-gateway",
"version": "1.19.4",
"version": "1.19.6",
"engines": {
"node": ">=16.0.0 <17.0.0"
},

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.29.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.29.0...@standardnotes/auth-server@1.29.1) (2022-09-19)
### Bug Fixes
* **auth:** uuid validator binding ([db6f966](https://github.com/standardnotes/server/commit/db6f966045d51e59555740c9e009bf66b629673c))
# [1.29.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.28.4...@standardnotes/auth-server@1.29.0) (2022-09-19)
### Features
* **files:** add validating remote identifiers ([db15457](https://github.com/standardnotes/server/commit/db15457ce4eb533ec822cf93c3ed83eafe9e64d5))
## [1.28.4](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.28.3...@standardnotes/auth-server@1.28.4) (2022-09-16)
### Bug Fixes
* **auth:** feature service spec ([c207c3f](https://github.com/standardnotes/server/commit/c207c3fc8442eec9b8c3150f09ecccfdd6a5ed50))
## [1.28.3](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.28.2...@standardnotes/auth-server@1.28.3) (2022-09-16)
### Bug Fixes
* **auth:** change remaining subscription time stats to percentage ([5eb957c](https://github.com/standardnotes/server/commit/5eb957c82a8cc5fdcb6815e2cd30e49cd2b1e8ac))
## [1.28.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.28.1...@standardnotes/auth-server@1.28.2) (2022-09-16)
### Bug Fixes

View File

@@ -0,0 +1,13 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class addRenewedAtColumn1663321030000 implements MigrationInterface {
name = 'addRenewedAtColumn1663321030000'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('ALTER TABLE `user_subscriptions` ADD `renewed_at` bigint NULL')
}
public async down(): Promise<void> {
return
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/auth-server",
"version": "1.28.2",
"version": "1.29.1",
"engines": {
"node": ">=16.0.0 <17.0.0"
},

View File

@@ -130,7 +130,14 @@ import { RedisOfflineSubscriptionTokenRepository } from '../Infra/Redis/RedisOff
import { CreateOfflineSubscriptionToken } from '../Domain/UseCase/CreateOfflineSubscriptionToken/CreateOfflineSubscriptionToken'
import { AuthenticateOfflineSubscriptionToken } from '../Domain/UseCase/AuthenticateOfflineSubscriptionToken/AuthenticateOfflineSubscriptionToken'
import { SubscriptionCancelledEventHandler } from '../Domain/Handler/SubscriptionCancelledEventHandler'
import { ContentDecoder, ContentDecoderInterface, ProtocolVersion } from '@standardnotes/common'
import {
ContentDecoder,
ContentDecoderInterface,
ProtocolVersion,
Uuid,
UuidValidator,
ValidatorInterface,
} from '@standardnotes/common'
import { GetUserOfflineSubscription } from '../Domain/UseCase/GetUserOfflineSubscription/GetUserOfflineSubscription'
import { ApiGatewayOfflineAuthMiddleware } from '../Controller/ApiGatewayOfflineAuthMiddleware'
import { UserEmailChangedEventHandler } from '../Domain/Handler/UserEmailChangedEventHandler'
@@ -559,6 +566,7 @@ export class ContainerConfigLoader {
container
.bind<StatisticsStoreInterface>(TYPES.StatisticsStore)
.toConstantValue(new RedisStatisticsStore(periodKeyGenerator, container.get(TYPES.Redis)))
container.bind<ValidatorInterface<Uuid>>(TYPES.UuidValidator).toConstantValue(new UuidValidator())
if (env.get('SNS_TOPIC_ARN', true)) {
container

View File

@@ -189,6 +189,7 @@ const TYPES = {
UserSubscriptionService: Symbol.for('UserSubscriptionService'),
AnalyticsStore: Symbol.for('AnalyticsStore'),
StatisticsStore: Symbol.for('StatisticsStore'),
UuidValidator: Symbol.for('UuidValidator'),
}
export default TYPES

View File

@@ -4,18 +4,23 @@ import { Request, Response } from 'express'
import { results } from 'inversify-express-utils'
import { ValetTokenController } from './ValetTokenController'
import { CreateValetToken } from '../Domain/UseCase/CreateValetToken/CreateValetToken'
import { Uuid, ValidatorInterface } from '@standardnotes/common'
describe('ValetTokenController', () => {
let createValetToken: CreateValetToken
let uuidValidator: ValidatorInterface<Uuid>
let request: Request
let response: Response
const createController = () => new ValetTokenController(createValetToken)
const createController = () => new ValetTokenController(createValetToken, uuidValidator)
beforeEach(() => {
createValetToken = {} as jest.Mocked<CreateValetToken>
createValetToken.execute = jest.fn().mockReturnValue({ success: true, valetToken: 'foobar' })
uuidValidator = {} as jest.Mocked<ValidatorInterface<Uuid>>
uuidValidator.validate = jest.fn().mockReturnValue(true)
request = {
body: {
operation: 'write',
@@ -42,6 +47,17 @@ describe('ValetTokenController', () => {
expect(await result.content.readAsStringAsync()).toEqual('{"success":true,"valetToken":"foobar"}')
})
it('should not create a valet token if the remote resource identifier is not a valid uuid', async () => {
uuidValidator.validate = jest.fn().mockReturnValue(false)
const httpResponse = <results.JsonResult>await createController().create(request, response)
const result = await httpResponse.executeAsync()
expect(createValetToken.execute).not.toHaveBeenCalled()
expect(result.statusCode).toEqual(400)
})
it('should create a read valet token for read only access session', async () => {
response.locals.readOnlyAccess = true
request.body.operation = 'read'

View File

@@ -11,12 +11,15 @@ import { CreateValetTokenPayload } from '@standardnotes/responses'
import TYPES from '../Bootstrap/Types'
import { CreateValetToken } from '../Domain/UseCase/CreateValetToken/CreateValetToken'
import { ErrorTag } from '@standardnotes/common'
import { ErrorTag, Uuid, ValidatorInterface } from '@standardnotes/common'
import { ValetTokenOperation } from '@standardnotes/security'
@controller('/valet-tokens', TYPES.ApiGatewayAuthMiddleware)
export class ValetTokenController extends BaseHttpController {
constructor(@inject(TYPES.CreateValetToken) private createValetKey: CreateValetToken) {
constructor(
@inject(TYPES.CreateValetToken) private createValetKey: CreateValetToken,
@inject(TYPES.UuidValidator) private uuidValitor: ValidatorInterface<Uuid>,
) {
super()
}
@@ -36,6 +39,20 @@ export class ValetTokenController extends BaseHttpController {
)
}
for (const resource of payload.resources) {
if (!this.uuidValitor.validate(resource.remoteIdentifier)) {
return this.json(
{
error: {
tag: ErrorTag.ParametersInvalid,
message: 'Invalid remote resource identifier.',
},
},
400,
)
}
}
const createValetKeyResponse = await this.createValetKey.execute({
userUuid: response.locals.user.uuid,
operation: payload.operation as ValetTokenOperation,

View File

@@ -82,6 +82,7 @@ describe('FeatureService', () => {
uuid: 'subscription-1-1-1',
createdAt: 111,
updatedAt: 222,
renewedAt: null,
planName: SubscriptionName.PlusPlan,
endsAt: 555,
user: Promise.resolve(user),
@@ -95,6 +96,7 @@ describe('FeatureService', () => {
uuid: 'subscription-2-2-2',
createdAt: 222,
updatedAt: 333,
renewedAt: null,
planName: SubscriptionName.ProPlan,
endsAt: 777,
user: Promise.resolve(user),
@@ -108,6 +110,7 @@ describe('FeatureService', () => {
uuid: 'subscription-3-3-3-canceled',
createdAt: 111,
updatedAt: 222,
renewedAt: null,
planName: SubscriptionName.PlusPlan,
endsAt: 333,
user: Promise.resolve(user),
@@ -121,6 +124,7 @@ describe('FeatureService', () => {
uuid: 'subscription-4-4-4-canceled',
createdAt: 111,
updatedAt: 222,
renewedAt: null,
planName: SubscriptionName.PlusPlan,
endsAt: 333,
user: Promise.resolve(user),
@@ -240,6 +244,7 @@ describe('FeatureService', () => {
uuid: 'subscription-1-1-1',
createdAt: 111,
updatedAt: 222,
renewedAt: null,
planName: 'non existing plan name' as SubscriptionName,
endsAt: 555,
user: Promise.resolve(user),

View File

@@ -27,14 +27,6 @@ export class SubscriptionCancelledEventHandler implements DomainEventHandlerInte
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
) {}
async handle(event: SubscriptionCancelledEvent): Promise<void> {
if (event.payload.offline) {
await this.updateOfflineSubscriptionCancelled(event.payload.subscriptionId, event.payload.timestamp)
return
}
await this.updateSubscriptionCancelled(event.payload.subscriptionId, event.payload.timestamp)
const user = await this.userRepository.findOneByEmail(event.payload.userEmail)
if (user !== null) {
const { analyticsId } = await this.getUserAnalyticsId.execute({ userUuid: user.uuid })
@@ -54,14 +46,27 @@ export class SubscriptionCancelledEventHandler implements DomainEventHandlerInte
Period.ThisMonth,
])
const lastPurchaseTime = lastSubscription.renewedAt ?? lastSubscription.updatedAt
const remainingSubscriptionTime = lastSubscription.endsAt - event.payload.timestamp
const totalSubscriptionTime = lastSubscription.endsAt - lastPurchaseTime
const remainingSubscriptionPercentage = Math.floor((remainingSubscriptionTime / totalSubscriptionTime) * 100)
await this.statisticsStore.incrementMeasure(
StatisticsMeasure.SubscriptionCancelToExpireTime,
remainingSubscriptionTime,
StatisticsMeasure.RemainingSubscriptionTimePercentage,
remainingSubscriptionPercentage,
[Period.Today, Period.ThisWeek, Period.ThisMonth],
)
}
}
if (event.payload.offline) {
await this.updateOfflineSubscriptionCancelled(event.payload.subscriptionId, event.payload.timestamp)
return
}
await this.updateSubscriptionCancelled(event.payload.subscriptionId, event.payload.timestamp)
}
private async updateSubscriptionCancelled(subscriptionId: number, timestamp: number): Promise<void> {

View File

@@ -34,6 +34,13 @@ export class UserSubscription {
@Index('updated_at')
declare updatedAt: number
@Column({
name: 'renewed_at',
type: 'bigint',
nullable: true,
})
declare renewedAt: number | null
@Column({
type: 'tinyint',
width: 1,

View File

@@ -138,7 +138,8 @@ describe('MySQLUserSubscriptionRepository', () => {
expect(updateQueryBuilder.update).toHaveBeenCalled()
expect(updateQueryBuilder.set).toHaveBeenCalledWith({
updatedAt: expect.any(Number),
updatedAt: 1000,
renewedAt: 1000,
endsAt: 1000,
})
expect(updateQueryBuilder.where).toHaveBeenCalledWith('subscription_id = :subscriptionId', {

View File

@@ -88,13 +88,14 @@ export class MySQLUserSubscriptionRepository implements UserSubscriptionReposito
return null
}
async updateEndsAt(subscriptionId: number, endsAt: number, updatedAt: number): Promise<void> {
async updateEndsAt(subscriptionId: number, endsAt: number, timestamp: number): Promise<void> {
await this.ormRepository
.createQueryBuilder()
.update()
.set({
endsAt,
updatedAt,
updatedAt: timestamp,
renewedAt: timestamp,
})
.where('subscription_id = :subscriptionId', {
subscriptionId,

View File

@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.33.0](https://github.com/standardnotes/server/compare/@standardnotes/common@1.32.0...@standardnotes/common@1.33.0) (2022-09-19)
### Features
* **files:** add validating remote identifiers ([db15457](https://github.com/standardnotes/server/commit/db15457ce4eb533ec822cf93c3ed83eafe9e64d5))
# [1.32.0](https://github.com/standardnotes/server/compare/@standardnotes/common@1.31.0...@standardnotes/common@1.32.0) (2022-09-09)
### Features

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/common",
"version": "1.32.0",
"version": "1.33.0",
"engines": {
"node": ">=16.0.0 <17.0.0"
},

View File

@@ -0,0 +1,34 @@
import { UuidValidator } from './UuidValidator'
describe('UuidValidator', () => {
const createValidator = () => new UuidValidator()
const validUuids = [
'2221101c-1da9-4d2b-9b32-b8be2a8d1c82',
'c08f2f29-a74b-42b4-aefd-98af9832391c',
'b453fa64-1493-443b-b5bb-bca7b9c696c7',
]
const invalidUuids = [
123,
'someone@127.0.0.1',
'',
null,
'b453fa64-1493-443b-b5bb-ca7b9c696c7',
'c08f*f29-a74b-42b4-aefd-98af9832391c',
'c08f*f29-a74b-42b4-aefd-98af9832391c',
'../../escaped.sh',
]
it('should validate proper uuids', () => {
for (const validUuid of validUuids) {
expect(createValidator().validate(validUuid)).toBeTruthy()
}
})
it('should not validate invalid uuids', () => {
for (const invalidUuid of invalidUuids) {
expect(createValidator().validate(invalidUuid as string)).toBeFalsy()
}
})
})

View File

@@ -0,0 +1,10 @@
import { Uuid } from '../DataType/Uuid'
import { ValidatorInterface } from './ValidatorInterface'
export class UuidValidator implements ValidatorInterface<Uuid> {
private readonly UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i
validate(data: Uuid): boolean {
return String(data).toLowerCase().match(this.UUID_REGEX) !== null
}
}

View File

@@ -0,0 +1,3 @@
export interface ValidatorInterface<T> {
validate(data: T): boolean
}

View File

@@ -20,3 +20,5 @@ export * from './Role/RoleName'
export * from './Subscription/SubscriptionName'
export * from './Type/Either'
export * from './Type/Only'
export * from './Validator/UuidValidator'
export * from './Validator/ValidatorInterface'

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.8.11](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.8.10...@standardnotes/domain-events-infra@1.8.11) (2022-09-19)
**Note:** Version bump only for package @standardnotes/domain-events-infra
## [1.8.10](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.8.9...@standardnotes/domain-events-infra@1.8.10) (2022-09-16)
**Note:** Version bump only for package @standardnotes/domain-events-infra

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/domain-events-infra",
"version": "1.8.10",
"version": "1.8.11",
"engines": {
"node": ">=16.0.0 <17.0.0"
},

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.60.5](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.60.4...@standardnotes/domain-events@2.60.5) (2022-09-19)
**Note:** Version bump only for package @standardnotes/domain-events
## [2.60.4](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.60.3...@standardnotes/domain-events@2.60.4) (2022-09-16)
**Note:** Version bump only for package @standardnotes/domain-events

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/domain-events",
"version": "2.60.4",
"version": "2.60.5",
"engines": {
"node": ">=16.0.0 <17.0.0"
},

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.3.16](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.3.15...@standardnotes/event-store@1.3.16) (2022-09-19)
**Note:** Version bump only for package @standardnotes/event-store
## [1.3.15](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.3.14...@standardnotes/event-store@1.3.15) (2022-09-16)
**Note:** Version bump only for package @standardnotes/event-store

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/event-store",
"version": "1.3.15",
"version": "1.3.16",
"description": "Event Store Service",
"private": true,
"main": "dist/src/index.js",

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.6.2](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.6.1...@standardnotes/files-server@1.6.2) (2022-09-19)
### Bug Fixes
* add upper bound for FS file chunk upload ([dfa7e06](https://github.com/standardnotes/files/commit/dfa7e06f8780bec21893ec77ab4a0945a6681545))
## [1.6.1](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.6.0...@standardnotes/files-server@1.6.1) (2022-09-19)
### Bug Fixes
* **files:** uuid validator binding ([a628bdc](https://github.com/standardnotes/files/commit/a628bdc44e97935b8a79460b74c30c0d29ef83bf))
# [1.6.0](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.5.52...@standardnotes/files-server@1.6.0) (2022-09-19)
### Features
* **files:** add validating remote identifiers ([db15457](https://github.com/standardnotes/files/commit/db15457ce4eb533ec822cf93c3ed83eafe9e64d5))
## [1.5.52](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.5.51...@standardnotes/files-server@1.5.52) (2022-09-16)
### Bug Fixes

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/files-server",
"version": "1.5.52",
"version": "1.6.2",
"engines": {
"node": ">=16.0.0 <17.0.0"
},

View File

@@ -44,6 +44,7 @@ import {
import { MarkFilesToBeRemoved } from '../Domain/UseCase/MarkFilesToBeRemoved/MarkFilesToBeRemoved'
import { AccountDeletionRequestedEventHandler } from '../Domain/Handler/AccountDeletionRequestedEventHandler'
import { SharedSubscriptionInvitationCanceledEventHandler } from '../Domain/Handler/SharedSubscriptionInvitationCanceledEventHandler'
import { Uuid, UuidValidator, ValidatorInterface } from '@standardnotes/common'
export class ContainerConfigLoader {
async load(): Promise<Container> {
@@ -107,6 +108,7 @@ export class ContainerConfigLoader {
.toConstantValue(new FSFileUploader(container.get(TYPES.FILE_UPLOAD_PATH), container.get(TYPES.Logger)))
container.bind<FileRemoverInterface>(TYPES.FileRemover).to(FSFileRemover)
}
container.bind<ValidatorInterface<Uuid>>(TYPES.UuidValidator).toConstantValue(new UuidValidator())
if (env.get('SNS_AWS_REGION', true)) {
container.bind<AWS.SNS>(TYPES.SNS).toConstantValue(

View File

@@ -23,6 +23,7 @@ const TYPES = {
FileUploader: Symbol.for('FileUploader'),
FileDownloader: Symbol.for('FileDownloader'),
FileRemover: Symbol.for('FileRemover'),
UuidValidator: Symbol.for('UuidValidator'),
// repositories
UploadRepository: Symbol.for('UploadRepository'),

View File

@@ -298,6 +298,7 @@ describe('FilesController', () => {
chunkId: 2,
data: Buffer.from([123]),
resourceRemoteIdentifier: '2-3-4',
resourceUnencryptedFileSize: 123,
userUuid: '1-2-3',
})
})

View File

@@ -63,6 +63,7 @@ export class FilesController extends BaseHttpController {
const result = await this.uploadFileChunk.execute({
userUuid: response.locals.userUuid,
resourceRemoteIdentifier: response.locals.permittedResources[0].remoteIdentifier,
resourceUnencryptedFileSize: response.locals.permittedResources[0].unencryptedFileSize,
chunkId,
data: request.body,
})

View File

@@ -4,9 +4,11 @@ import { ValetTokenAuthMiddleware } from './ValetTokenAuthMiddleware'
import { NextFunction, Request, Response } from 'express'
import { Logger } from 'winston'
import { TokenDecoderInterface, ValetTokenData } from '@standardnotes/security'
import { Uuid, ValidatorInterface } from '@standardnotes/common'
describe('ValetTokenAuthMiddleware', () => {
let tokenDecoder: TokenDecoderInterface<ValetTokenData>
let uuidValidator: ValidatorInterface<Uuid>
let request: Request
let response: Response
let next: NextFunction
@@ -15,7 +17,7 @@ describe('ValetTokenAuthMiddleware', () => {
debug: jest.fn(),
} as unknown as jest.Mocked<Logger>
const createMiddleware = () => new ValetTokenAuthMiddleware(tokenDecoder, logger)
const createMiddleware = () => new ValetTokenAuthMiddleware(tokenDecoder, uuidValidator, logger)
beforeEach(() => {
tokenDecoder = {} as jest.Mocked<TokenDecoderInterface<ValetTokenData>>
@@ -32,6 +34,9 @@ describe('ValetTokenAuthMiddleware', () => {
uploadBytesUsed: 80,
})
uuidValidator = {} as jest.Mocked<ValidatorInterface<Uuid>>
uuidValidator.validate = jest.fn().mockReturnValue(true)
request = {
headers: {},
query: {},
@@ -174,6 +179,30 @@ describe('ValetTokenAuthMiddleware', () => {
expect(next).not.toHaveBeenCalled()
})
it('should not authorize if valet token has an invalid remote resource identifier', async () => {
tokenDecoder.decodeToken = jest.fn().mockReturnValue({
userUuid: '1-2-3',
permittedResources: [
{
remoteIdentifier: '1-2-3/2-3-4',
unencryptedFileSize: 30,
},
],
permittedOperation: 'write',
uploadBytesLimit: -1,
uploadBytesUsed: 80,
})
request.headers['x-valet-token'] = 'valet-token'
uuidValidator.validate = jest.fn().mockReturnValue(false)
await createMiddleware().handler(request, response, next)
expect(response.status).toHaveBeenCalledWith(401)
expect(next).not.toHaveBeenCalled()
})
it('should not authorize if auth valet token is malformed', async () => {
request.headers['x-valet-token'] = 'valet-token'

View File

@@ -1,3 +1,4 @@
import { Uuid, ValidatorInterface } from '@standardnotes/common'
import { TokenDecoderInterface, ValetTokenData } from '@standardnotes/security'
import { NextFunction, Request, Response } from 'express'
import { inject, injectable } from 'inversify'
@@ -9,6 +10,7 @@ import TYPES from '../Bootstrap/Types'
export class ValetTokenAuthMiddleware extends BaseMiddleware {
constructor(
@inject(TYPES.ValetTokenDecoder) private tokenDecoder: TokenDecoderInterface<ValetTokenData>,
@inject(TYPES.UuidValidator) private uuidValidator: ValidatorInterface<Uuid>,
@inject(TYPES.Logger) private logger: Logger,
) {
super()
@@ -45,6 +47,21 @@ export class ValetTokenAuthMiddleware extends BaseMiddleware {
return
}
for (const resource of valetTokenData.permittedResources) {
if (!this.uuidValidator.validate(resource.remoteIdentifier)) {
this.logger.debug('Invalid remote resource identifier in token.')
response.status(401).send({
error: {
tag: 'invalid-auth',
message: 'Invalid valet token.',
},
})
return
}
}
if (this.userHasNoSpaceToUpload(valetTokenData)) {
response.status(403).send({
error: {

View File

@@ -4,6 +4,12 @@ import { UploadId } from '../Upload/UploadId'
export interface FileUploaderInterface {
createUploadSession(filePath: string): Promise<UploadId>
uploadFileChunk(dto: { uploadId: string; data: Uint8Array; filePath: string; chunkId: ChunkId }): Promise<string>
uploadFileChunk(dto: {
uploadId: string
data: Uint8Array
filePath: string
chunkId: ChunkId
unencryptedFileSize: number
}): Promise<string>
finishUploadSession(uploadId: string, filePath: string, uploadChunkResults: Array<UploadChunkResult>): Promise<void>
}

View File

@@ -33,6 +33,7 @@ describe('UploadFileChunk', () => {
chunkId: 2,
data: new Uint8Array([123]),
resourceRemoteIdentifier: '2-3-4',
resourceUnencryptedFileSize: 123,
userUuid: '1-2-3',
})
@@ -50,6 +51,7 @@ describe('UploadFileChunk', () => {
chunkId: 2,
data: new Uint8Array([123]),
resourceRemoteIdentifier: '2-3-4',
resourceUnencryptedFileSize: 123,
userUuid: '1-2-3',
}),
).toEqual({
@@ -66,6 +68,7 @@ describe('UploadFileChunk', () => {
chunkId: 2,
data: new Uint8Array([123]),
resourceRemoteIdentifier: '2-3-4',
resourceUnencryptedFileSize: 123,
userUuid: '1-2-3',
})
@@ -74,6 +77,7 @@ describe('UploadFileChunk', () => {
data: new Uint8Array([123]),
filePath: '1-2-3/2-3-4',
uploadId: '123',
unencryptedFileSize: 123,
})
expect(uploadRepository.storeUploadChunkResult).toHaveBeenCalledWith('123', {
tag: 'ETag123',

View File

@@ -39,6 +39,7 @@ export class UploadFileChunk implements UseCaseInterface {
data: dto.data,
chunkId: dto.chunkId,
filePath,
unencryptedFileSize: dto.resourceUnencryptedFileSize,
})
await this.uploadRepository.storeUploadChunkResult(uploadId, {

View File

@@ -5,4 +5,5 @@ export type UploadFileChunkDTO = {
chunkId: ChunkId
userUuid: string
resourceRemoteIdentifier: string
resourceUnencryptedFileSize: number
}

View File

@@ -1,11 +1,12 @@
import { promises } from 'fs'
import { dirname } from 'path'
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
import { FileUploaderInterface } from '../../Domain/Services/FileUploaderInterface'
import { UploadChunkResult } from '../../Domain/Upload/UploadChunkResult'
import { Logger } from 'winston'
import TYPES from '../../Bootstrap/Types'
import { ChunkId } from '../../Domain/Upload/ChunkId'
@injectable()
export class FSFileUploader implements FileUploaderInterface {
@@ -22,7 +23,8 @@ export class FSFileUploader implements FileUploaderInterface {
uploadId: string
data: Uint8Array
filePath: string
chunkId: number
chunkId: ChunkId
unencryptedFileSize: number
}): Promise<string> {
if (!this.inMemoryChunks.has(dto.uploadId)) {
this.inMemoryChunks.set(dto.uploadId, new Map<number, Uint8Array>())
@@ -30,6 +32,13 @@ export class FSFileUploader implements FileUploaderInterface {
const fileChunks = this.inMemoryChunks.get(dto.uploadId) as Map<number, Uint8Array>
const alreadyStoredBytes = this.accumulatedEncryptedFileSize(fileChunks)
if (alreadyStoredBytes >= dto.unencryptedFileSize) {
throw new Error(
`Could not finish chunk upload. Accumulated encrypted file size (${alreadyStoredBytes}B) already exceeds the unecrypted file size: ${dto.unencryptedFileSize}`,
)
}
this.logger.debug(`FS storing file chunk ${dto.chunkId} in memory for ${dto.uploadId}`)
fileChunks.set(dto.chunkId, dto.data)
@@ -64,4 +73,14 @@ export class FSFileUploader implements FileUploaderInterface {
return fullPath
}
private accumulatedEncryptedFileSize(fileChunks: Map<number, Uint8Array>): number {
let accumulatedSize = 0
for (const value of fileChunks.values()) {
accumulatedSize += value.byteLength
}
return accumulatedSize
}
}

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.4.2](https://github.com/standardnotes/server/compare/@standardnotes/predicates@1.4.1...@standardnotes/predicates@1.4.2) (2022-09-19)
**Note:** Version bump only for package @standardnotes/predicates
## [1.4.1](https://github.com/standardnotes/server/compare/@standardnotes/predicates@1.4.0...@standardnotes/predicates@1.4.1) (2022-09-09)
**Note:** Version bump only for package @standardnotes/predicates

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/predicates",
"version": "1.4.1",
"version": "1.4.2",
"engines": {
"node": ">=16.0.0 <17.0.0"
},

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.10.30](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.10.29...@standardnotes/scheduler-server@1.10.30) (2022-09-19)
**Note:** Version bump only for package @standardnotes/scheduler-server
## [1.10.29](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.10.28...@standardnotes/scheduler-server@1.10.29) (2022-09-16)
**Note:** Version bump only for package @standardnotes/scheduler-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/scheduler-server",
"version": "1.10.29",
"version": "1.10.30",
"engines": {
"node": ">=16.0.0 <17.0.0"
},

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.3.3](https://github.com/standardnotes/server/compare/@standardnotes/security@1.3.2...@standardnotes/security@1.3.3) (2022-09-19)
**Note:** Version bump only for package @standardnotes/security
## [1.3.2](https://github.com/standardnotes/server/compare/@standardnotes/security@1.3.1...@standardnotes/security@1.3.2) (2022-09-16)
### Bug Fixes

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/security",
"version": "1.3.2",
"version": "1.3.3",
"engines": {
"node": ">=16.0.0 <17.0.0"
},

View File

@@ -3,6 +3,20 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.8.7](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.8.6...@standardnotes/syncing-server@1.8.7) (2022-09-20)
### Bug Fixes
* **syncing-server:** content size calculation and add syncing upper bound for limit paramter ([c2e9f3e](https://github.com/standardnotes/syncing-server-js/commit/c2e9f3e72b87c445a6f4d61cbf59621954187d21))
## [1.8.6](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.8.5...@standardnotes/syncing-server@1.8.6) (2022-09-19)
**Note:** Version bump only for package @standardnotes/syncing-server
## [1.8.5](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.8.4...@standardnotes/syncing-server@1.8.5) (2022-09-16)
**Note:** Version bump only for package @standardnotes/syncing-server
## [1.8.4](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.8.3...@standardnotes/syncing-server@1.8.4) (2022-09-16)
**Note:** Version bump only for package @standardnotes/syncing-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/syncing-server",
"version": "1.8.4",
"version": "1.8.7",
"engines": {
"node": ">=16.0.0 <17.0.0"
},

View File

@@ -5,12 +5,16 @@ import { ContentType } from '@standardnotes/common'
import { ItemFactory } from './ItemFactory'
import { ItemHash } from './ItemHash'
import { ProjectorInterface } from '../../Projection/ProjectorInterface'
import { ItemProjection } from '../../Projection/ItemProjection'
import { Item } from './Item'
describe('ItemFactory', () => {
let timer: TimerInterface
let itemProjector: ProjectorInterface<Item, ItemProjection>
let timeHelper: Timer
const createFactory = () => new ItemFactory(timer)
const createFactory = () => new ItemFactory(timer, itemProjector)
beforeEach(() => {
timeHelper = new Timer()
@@ -26,6 +30,23 @@ describe('ItemFactory', () => {
timer.convertStringDateToDate = jest
.fn()
.mockImplementation((date: string) => timeHelper.convertStringDateToDate(date))
itemProjector = {} as jest.Mocked<ProjectorInterface<Item, ItemProjection>>
itemProjector.projectFull = jest.fn().mockReturnValue({
uuid: '1-2-3',
items_key_id: 'foobar',
duplicate_of: null,
enc_item_key: 'foobar',
content: 'foobar',
content_type: ContentType.Note,
auth_hash: 'foobar',
deleted: false,
created_at: '2022-09-01 10:00:00',
created_at_timestamp: 123123123123123,
updated_at: '2022-09-01 10:00:00',
updated_at_timestamp: 123123123123123,
updated_with_session: '2-4-5',
})
})
it('should create an item based on item hash', () => {
@@ -43,7 +64,7 @@ describe('ItemFactory', () => {
updatedAtTimestamp: 1616164633241568,
userUuid: 'a-b-c',
uuid: '1-2-3',
contentSize: 0,
contentSize: 341,
})
})
@@ -64,7 +85,7 @@ describe('ItemFactory', () => {
userUuid: 'a-b-c',
uuid: '1-2-3',
content: null,
contentSize: 0,
contentSize: 341,
})
})
@@ -86,7 +107,7 @@ describe('ItemFactory', () => {
userUuid: 'a-b-c',
uuid: '1-2-3',
content: 'foobar',
contentSize: 6,
contentSize: 341,
})
})
@@ -106,7 +127,7 @@ describe('ItemFactory', () => {
userUuid: 'a-b-c',
uuid: '1-2-3',
content: null,
contentSize: 0,
contentSize: 341,
})
})
@@ -128,7 +149,7 @@ describe('ItemFactory', () => {
expect(item).toEqual({
content: 'asdqwe1',
contentSize: 7,
contentSize: 341,
contentType: 'Note',
createdAt: expect.any(Date),
updatedWithSession: '1-2-3',
@@ -161,7 +182,7 @@ describe('ItemFactory', () => {
expect(item).toEqual({
content: 'asdqwe1',
contentSize: 7,
contentSize: 341,
contentType: 'Note',
createdAt: expect.any(Date),
updatedWithSession: '1-2-3',

View File

@@ -3,13 +3,18 @@ import { TimerInterface } from '@standardnotes/time'
import { inject, injectable } from 'inversify'
import TYPES from '../../Bootstrap/Types'
import { ItemProjection } from '../../Projection/ItemProjection'
import { ProjectorInterface } from '../../Projection/ProjectorInterface'
import { Item } from './Item'
import { ItemFactoryInterface } from './ItemFactoryInterface'
import { ItemHash } from './ItemHash'
@injectable()
export class ItemFactory implements ItemFactoryInterface {
constructor(@inject(TYPES.Timer) private timer: TimerInterface) {}
constructor(
@inject(TYPES.Timer) private timer: TimerInterface,
@inject(TYPES.ItemProjector) private itemProjector: ProjectorInterface<Item, ItemProjection>,
) {}
createStub(dto: { userUuid: string; itemHash: ItemHash; sessionUuid: Uuid | null }): Item {
const item = this.create(dto)
@@ -36,7 +41,6 @@ export class ItemFactory implements ItemFactoryInterface {
newItem.contentSize = 0
if (dto.itemHash.content) {
newItem.content = dto.itemHash.content
newItem.contentSize = Buffer.byteLength(dto.itemHash.content)
}
newItem.userUuid = dto.userUuid
if (dto.itemHash.content_type) {
@@ -75,6 +79,8 @@ export class ItemFactory implements ItemFactoryInterface {
newItem.createdAt = this.timer.convertStringDateToDate(dto.itemHash.created_at)
}
newItem.contentSize = Buffer.byteLength(JSON.stringify(this.itemProjector.projectFull(newItem)))
return newItem
}
}

View File

@@ -16,6 +16,8 @@ import { ItemSaveValidatorInterface } from './SaveValidator/ItemSaveValidatorInt
import { ItemFactoryInterface } from './ItemFactoryInterface'
import { ItemConflict } from './ItemConflict'
import { ItemTransferCalculatorInterface } from './ItemTransferCalculatorInterface'
import { ProjectorInterface } from '../../Projection/ProjectorInterface'
import { ItemProjection } from '../../Projection/ItemProjection'
describe('ItemService', () => {
let itemRepository: ItemRepositoryInterface
@@ -37,6 +39,7 @@ describe('ItemService', () => {
let itemFactory: ItemFactoryInterface
let timeHelper: Timer
let itemTransferCalculator: ItemTransferCalculatorInterface
let itemProjector: ProjectorInterface<Item, ItemProjection>
const createService = () =>
new ItemService(
@@ -50,6 +53,7 @@ describe('ItemService', () => {
contentSizeTransferLimit,
itemTransferCalculator,
timer,
itemProjector,
logger,
)
@@ -156,6 +160,24 @@ describe('ItemService', () => {
itemFactory = {} as jest.Mocked<ItemFactoryInterface>
itemFactory.create = jest.fn().mockReturnValue(newItem)
itemFactory.createStub = jest.fn().mockReturnValue(newItem)
itemProjector = {} as jest.Mocked<ProjectorInterface<Item, ItemProjection>>
itemProjector.projectFull = jest.fn().mockReturnValue({
uuid: '1-2-3',
items_key_id: 'foobar',
duplicate_of: null,
enc_item_key: 'foobar',
content:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Sed viverra tellus in hac habitasse. Tortor posuere ac ut consequat semper. Ut diam quam nulla porttitor. Sapien pellentesque habitant morbi tristique senectus et netus et malesuada. Dapibus ultrices in iaculis nunc. Pellentesque habitant morbi tristique senectus et netus et malesuada fames. Faucibus et molestie ac feugiat sed lectus vestibulum mattis. Eu consequat ac felis donec. Eget velit aliquet sagittis id. Nullam eget felis eget nunc. Turpis in eu mi bibendum neque egestas congue.',
content_type: ContentType.Note,
auth_hash: 'foobar',
deleted: false,
created_at: '2022-09-01 10:00:00',
created_at_timestamp: 123123123123123,
updated_at: '2022-09-01 10:00:00',
updated_at_timestamp: 123123123123123,
updated_with_session: '2-4-5',
})
})
it('should retrieve all items for a user from last sync with sync token version 1', async () => {
@@ -214,6 +236,34 @@ describe('ItemService', () => {
})
})
it('should retrieve all items for a user from last sync with upper bound items limit', async () => {
expect(
await createService().getItems({
userUuid: '1-2-3',
syncToken,
contentType: ContentType.Note,
limit: 1000,
}),
).toEqual({
items: [item1, item2],
})
expect(itemRepository.countAll).toHaveBeenCalledWith({
contentType: 'Note',
lastSyncTime: 1616164633241564,
syncTimeComparison: '>',
sortBy: 'updated_at_timestamp',
sortOrder: 'ASC',
userUuid: '1-2-3',
limit: 300,
})
expect(itemRepository.findAll).toHaveBeenCalledWith({
uuids: ['1-2-3', '2-3-4'],
sortBy: 'updated_at_timestamp',
sortOrder: 'ASC',
})
})
it('should retrieve no items for a user if there are none from last sync', async () => {
itemTransferCalculator.computeItemUuidsToFetch = jest.fn().mockReturnValue([])
@@ -589,7 +639,7 @@ describe('ItemService', () => {
savedItems: [
{
content: 'asdqwe1',
contentSize: 7,
contentSize: 950,
contentType: 'Note',
createdAtTimestamp: expect.any(Number),
createdAt: expect.any(Date),
@@ -625,7 +675,7 @@ describe('ItemService', () => {
savedItems: [
{
content: 'asdqwe1',
contentSize: 7,
contentSize: 950,
contentType: 'Note',
createdAtTimestamp: expect.any(Number),
createdAt: expect.any(Date),
@@ -660,7 +710,7 @@ describe('ItemService', () => {
savedItems: [
{
content: 'asdqwe1',
contentSize: 7,
contentSize: 950,
contentType: 'Note',
createdAtTimestamp: 123,
createdAt: expect.any(Date),
@@ -696,7 +746,7 @@ describe('ItemService', () => {
conflicts: [],
savedItems: [
{
contentSize: 0,
contentSize: 950,
createdAtTimestamp: expect.any(Number),
createdAt: expect.any(Date),
userUuid: '1-2-3',
@@ -726,7 +776,7 @@ describe('ItemService', () => {
savedItems: [
{
content: 'asdqwe1',
contentSize: 7,
contentSize: 950,
contentType: 'Note',
createdAtTimestamp: expect.any(Number),
createdAt: expect.any(Date),
@@ -759,7 +809,7 @@ describe('ItemService', () => {
savedItems: [
{
content: 'asdqwe1',
contentSize: 7,
contentSize: 950,
contentType: 'Note',
createdAtTimestamp: expect.any(Number),
createdAt: expect.any(Date),
@@ -794,7 +844,7 @@ describe('ItemService', () => {
savedItems: [
{
content: 'asdqwe1',
contentSize: 7,
contentSize: 950,
contentType: 'Note',
createdAtTimestamp: expect.any(Number),
createdAt: expect.any(Date),
@@ -865,7 +915,7 @@ describe('ItemService', () => {
savedItems: [
{
content: 'asdqwe1',
contentSize: 7,
contentSize: 950,
contentType: 'Note',
createdAtTimestamp: expect.any(Number),
createdAt: expect.any(Date),

View File

@@ -21,10 +21,13 @@ import { SaveItemsResult } from './SaveItemsResult'
import { ItemSaveValidatorInterface } from './SaveValidator/ItemSaveValidatorInterface'
import { ConflictType } from '@standardnotes/responses'
import { ItemTransferCalculatorInterface } from './ItemTransferCalculatorInterface'
import { ProjectorInterface } from '../../Projection/ProjectorInterface'
import { ItemProjection } from '../../Projection/ItemProjection'
@injectable()
export class ItemService implements ItemServiceInterface {
private readonly DEFAULT_ITEMS_LIMIT = 150
private readonly MAX_ITEMS_LIMIT = 300
private readonly SYNC_TOKEN_VERSION = 2
constructor(
@@ -38,6 +41,7 @@ export class ItemService implements ItemServiceInterface {
@inject(TYPES.CONTENT_SIZE_TRANSFER_LIMIT) private contentSizeTransferLimit: number,
@inject(TYPES.ItemTransferCalculator) private itemTransferCalculator: ItemTransferCalculatorInterface,
@inject(TYPES.Timer) private timer: TimerInterface,
@inject(TYPES.ItemProjector) private itemProjector: ProjectorInterface<Item, ItemProjection>,
@inject(TYPES.Logger) private logger: Logger,
) {}
@@ -54,7 +58,7 @@ export class ItemService implements ItemServiceInterface {
deleted: lastSyncTime ? undefined : false,
sortBy: 'updated_at_timestamp',
sortOrder: 'ASC',
limit,
limit: limit < this.MAX_ITEMS_LIMIT ? limit : this.MAX_ITEMS_LIMIT,
}
const itemUuidsToFetch = await this.itemTransferCalculator.computeItemUuidsToFetch(
@@ -196,7 +200,6 @@ export class ItemService implements ItemServiceInterface {
dto.existingItem.contentSize = 0
if (dto.itemHash.content) {
dto.existingItem.content = dto.itemHash.content
dto.existingItem.contentSize = Buffer.byteLength(dto.itemHash.content)
}
if (dto.itemHash.content_type) {
dto.existingItem.contentType = dto.itemHash.content_type
@@ -219,14 +222,6 @@ export class ItemService implements ItemServiceInterface {
dto.existingItem.itemsKeyId = dto.itemHash.items_key_id
}
if (dto.itemHash.deleted === true) {
dto.existingItem.deleted = true
dto.existingItem.content = null
;(dto.existingItem.contentSize = 0), (dto.existingItem.encItemKey = null)
dto.existingItem.authHash = null
dto.existingItem.itemsKeyId = null
}
const updatedAt = this.timer.getTimestampInMicroseconds()
const secondsFromLastUpdate = this.timer.convertMicrosecondsToSeconds(
updatedAt - dto.existingItem.updatedAtTimestamp,
@@ -243,6 +238,17 @@ export class ItemService implements ItemServiceInterface {
dto.existingItem.updatedAtTimestamp = updatedAt
dto.existingItem.updatedAt = this.timer.convertMicrosecondsToDate(updatedAt)
dto.existingItem.contentSize = Buffer.byteLength(JSON.stringify(this.itemProjector.projectFull(dto.existingItem)))
if (dto.itemHash.deleted === true) {
dto.existingItem.deleted = true
dto.existingItem.content = null
dto.existingItem.contentSize = 0
dto.existingItem.encItemKey = null
dto.existingItem.authHash = null
dto.existingItem.itemsKeyId = null
}
const savedItem = await this.itemRepository.save(dto.existingItem)
if (secondsFromLastUpdate >= this.revisionFrequency) {