Compare commits

...

10 Commits

15 changed files with 201 additions and 20 deletions
+12
View File
@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.22.49](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.48...@standardnotes/home-server@1.22.49) (2024-01-05)
**Note:** Version bump only for package @standardnotes/home-server
## [1.22.48](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.47...@standardnotes/home-server@1.22.48) (2024-01-05)
**Note:** Version bump only for package @standardnotes/home-server
## [1.22.47](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.46...@standardnotes/home-server@1.22.47) (2024-01-05)
**Note:** Version bump only for package @standardnotes/home-server
## [1.22.46](https://github.com/standardnotes/server/compare/@standardnotes/home-server@1.22.45...@standardnotes/home-server@1.22.46) (2024-01-05)
**Note:** Version bump only for package @standardnotes/home-server
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/home-server",
"version": "1.22.46",
"version": "1.22.49",
"engines": {
"node": ">=18.0.0 <21.0.0"
},
+22
View File
@@ -3,6 +3,28 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.133.3](https://github.com/standardnotes/server/compare/@standardnotes/syncing-server@1.133.2...@standardnotes/syncing-server@1.133.3) (2024-01-05)
### Bug Fixes
* **syncing-server:** add traffic abuse check in gRPC coms ([e3cb1fa](https://github.com/standardnotes/server/commit/e3cb1faba46bbfd8741f6c827daa9438934dd710))
* **syncing-server:** remove excessive debug logs ([5c5f988](https://github.com/standardnotes/server/commit/5c5f9880556f14f5cbe4599ac0d639c970495056))
## [1.133.2](https://github.com/standardnotes/server/compare/@standardnotes/syncing-server@1.133.1...@standardnotes/syncing-server@1.133.2) (2024-01-05)
### Bug Fixes
* **syncing-server:** add debug logs to redis metrics store ([a4ad37f](https://github.com/standardnotes/server/commit/a4ad37f30948ba4292f367240c3dbfca916282ac))
* **syncing-server:** add metadata to transfer breach logs ([73c2cc1](https://github.com/standardnotes/server/commit/73c2cc1222b647e82de3755c7e28d283cd9f872f))
## [1.133.1](https://github.com/standardnotes/server/compare/@standardnotes/syncing-server@1.133.0...@standardnotes/syncing-server@1.133.1) (2024-01-05)
### Bug Fixes
* **syncing-server:** add debug logs for checking traffic abuse ([1f4b26d](https://github.com/standardnotes/server/commit/1f4b26d269a92f5b43455ce3a3cf3d4f15f0d099))
* **syncing-server:** error message ([e253825](https://github.com/standardnotes/server/commit/e253825da69d1be6d7bb4d0360f8c3add73516ef))
* **syncing-server:** metadata in logs ([02f4d5c](https://github.com/standardnotes/server/commit/02f4d5c717cef1014930f11b2792868967812042))
# [1.133.0](https://github.com/standardnotes/server/compare/@standardnotes/syncing-server@1.132.0...@standardnotes/syncing-server@1.133.0) (2024-01-05)
### Features
+7
View File
@@ -29,6 +29,7 @@ import { SyncingServer } from '../src/Infra/gRPC/SyncingServer'
import { SyncItems } from '../src/Domain/UseCase/Syncing/SyncItems/SyncItems'
import { SyncResponseFactoryResolverInterface } from '../src/Domain/Item/SyncResponse/SyncResponseFactoryResolverInterface'
import { SyncResponse20200115 } from '../src/Domain/Item/SyncResponse/SyncResponse20200115'
import { CheckForTrafficAbuse } from '../src/Domain/UseCase/Syncing/CheckForTrafficAbuse/CheckForTrafficAbuse'
const container = new ContainerConfigLoader()
void container.load().then((container) => {
@@ -114,6 +115,12 @@ void container.load().then((container) => {
container.get<SyncItems>(TYPES.Sync_SyncItems),
container.get<SyncResponseFactoryResolverInterface>(TYPES.Sync_SyncResponseFactoryResolver),
container.get<MapperInterface<SyncResponse20200115, SyncResponse>>(TYPES.Sync_SyncResponseGRPCMapper),
container.get<CheckForTrafficAbuse>(TYPES.Sync_CheckForTrafficAbuse),
container.get<boolean>(TYPES.Sync_STRICT_ABUSE_PROTECTION),
container.get<number>(TYPES.Sync_ITEM_OPERATIONS_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES),
container.get<number>(TYPES.Sync_ITEM_OPERATIONS_ABUSE_THRESHOLD),
container.get<number>(TYPES.Sync_PAYLOAD_SIZE_ABUSE_THRESHOLD),
container.get<number>(TYPES.Sync_PAYLOAD_SIZE_ABUSE_TIMEFRAME_LENGTH_IN_MINUTES),
container.get<winston.Logger>(TYPES.Sync_Logger),
)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/syncing-server",
"version": "1.133.0",
"version": "1.133.3",
"engines": {
"node": ">=18.0.0 <21.0.0"
},
@@ -585,7 +585,11 @@ export class ContainerConfigLoader {
container
.bind<MetricsStoreInterface>(TYPES.Sync_MetricsStore)
.toConstantValue(
new RedisMetricStore(container.get<Redis>(TYPES.Sync_Redis), container.get<TimerInterface>(TYPES.Sync_Timer)),
new RedisMetricStore(
container.get<Redis>(TYPES.Sync_Redis),
container.get<TimerInterface>(TYPES.Sync_Timer),
container.get<Logger>(TYPES.Sync_Logger),
),
)
}
@@ -967,6 +971,7 @@ export class ContainerConfigLoader {
new CheckForTrafficAbuse(
container.get<MetricsStoreInterface>(TYPES.Sync_MetricsStore),
container.get<TimerInterface>(TYPES.Sync_Timer),
container.get<Logger>(TYPES.Sync_Logger),
),
)
@@ -3,7 +3,7 @@ import {
DomainEventPublisherInterface,
EmailBackupRequestedEvent,
} from '@standardnotes/domain-events'
import { EmailLevel } from '@standardnotes/domain-core'
import { EmailLevel, Uuid } from '@standardnotes/domain-core'
import { Logger } from 'winston'
import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
import { ItemBackupServiceInterface } from '../Item/ItemBackupServiceInterface'
@@ -32,6 +32,17 @@ export class EmailBackupRequestedEventHandler implements DomainEventHandlerInter
event: EmailBackupRequestedEvent,
itemRepository: ItemRepositoryInterface,
): Promise<void> {
const userUuidOrError = Uuid.create(event.payload.userUuid)
if (userUuidOrError.isFailed()) {
this.logger.error('User uuid is invalid', {
userId: event.payload.userUuid,
codeTag: 'EmailBackupRequestedEventHandler',
})
return
}
const userUuid = userUuidOrError.getValue()
const itemQuery: ItemQuery = {
userUuid: event.payload.userUuid,
sortBy: 'updated_at_timestamp',
@@ -42,6 +53,7 @@ export class EmailBackupRequestedEventHandler implements DomainEventHandlerInter
const itemUuidBundles = await this.itemTransferCalculator.computeItemUuidBundlesToFetch(
itemContentSizeDescriptors,
this.emailAttachmentMaxByteSize,
userUuid,
)
const backupFileNames: string[] = []
@@ -4,13 +4,18 @@ import { Logger } from 'winston'
import { ItemTransferCalculator } from './ItemTransferCalculator'
import { ItemContentSizeDescriptor } from './ItemContentSizeDescriptor'
import { Uuid } from '@standardnotes/domain-core'
describe('ItemTransferCalculator', () => {
let logger: Logger
const createCalculator = () => new ItemTransferCalculator(logger)
let userUuid: Uuid
beforeEach(() => {
userUuid = Uuid.create('00000000-0000-0000-0000-000000000000').getValue()
logger = {} as jest.Mocked<Logger>
logger.warn = jest.fn()
})
@@ -23,7 +28,7 @@ describe('ItemTransferCalculator', () => {
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000002', 20).getValue(),
]
const result = await createCalculator().computeItemUuidsToFetch(itemContentSizeDescriptors, 50)
const result = await createCalculator().computeItemUuidsToFetch(itemContentSizeDescriptors, 50, userUuid)
expect(result).toEqual({
uuids: [
@@ -42,7 +47,7 @@ describe('ItemTransferCalculator', () => {
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000002', 20).getValue(),
]
const result = await createCalculator().computeItemUuidsToFetch(itemContentSizeDescriptors, 40)
const result = await createCalculator().computeItemUuidsToFetch(itemContentSizeDescriptors, 40, userUuid)
expect(result).toEqual({
uuids: ['00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000001'],
@@ -57,7 +62,7 @@ describe('ItemTransferCalculator', () => {
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000002', null).getValue(),
]
const result = await createCalculator().computeItemUuidsToFetch(itemContentSizeDescriptors, 50)
const result = await createCalculator().computeItemUuidsToFetch(itemContentSizeDescriptors, 50, userUuid)
expect(result).toEqual({
uuids: [
@@ -76,7 +81,7 @@ describe('ItemTransferCalculator', () => {
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000002', 20).getValue(),
]
const result = await createCalculator().computeItemUuidsToFetch(itemContentSizeDescriptors, 40)
const result = await createCalculator().computeItemUuidsToFetch(itemContentSizeDescriptors, 40, userUuid)
expect(result).toEqual({
uuids: ['00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000001'],
@@ -93,7 +98,7 @@ describe('ItemTransferCalculator', () => {
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000002', 20).getValue(),
]
const result = await createCalculator().computeItemUuidBundlesToFetch(itemContentSizeDescriptors, 50)
const result = await createCalculator().computeItemUuidBundlesToFetch(itemContentSizeDescriptors, 50, userUuid)
expect(result).toEqual([
[
@@ -111,7 +116,7 @@ describe('ItemTransferCalculator', () => {
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000002', 20).getValue(),
]
const result = await createCalculator().computeItemUuidBundlesToFetch(itemContentSizeDescriptors, 40)
const result = await createCalculator().computeItemUuidBundlesToFetch(itemContentSizeDescriptors, 40, userUuid)
expect(result).toEqual([
['00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000001'],
@@ -126,7 +131,7 @@ describe('ItemTransferCalculator', () => {
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000002', null).getValue(),
]
const result = await createCalculator().computeItemUuidBundlesToFetch(itemContentSizeDescriptors, 50)
const result = await createCalculator().computeItemUuidBundlesToFetch(itemContentSizeDescriptors, 50, userUuid)
expect(result).toEqual([
[
@@ -144,7 +149,7 @@ describe('ItemTransferCalculator', () => {
ItemContentSizeDescriptor.create('00000000-0000-0000-0000-000000000002', 20).getValue(),
]
const result = await createCalculator().computeItemUuidBundlesToFetch(itemContentSizeDescriptors, 40)
const result = await createCalculator().computeItemUuidBundlesToFetch(itemContentSizeDescriptors, 40, userUuid)
expect(result).toEqual([
['00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000001'],
@@ -2,6 +2,7 @@ import { Logger } from 'winston'
import { ItemTransferCalculatorInterface } from './ItemTransferCalculatorInterface'
import { ItemContentSizeDescriptor } from './ItemContentSizeDescriptor'
import { Uuid } from '@standardnotes/domain-core'
export class ItemTransferCalculator implements ItemTransferCalculatorInterface {
constructor(private logger: Logger) {}
@@ -9,6 +10,7 @@ export class ItemTransferCalculator implements ItemTransferCalculatorInterface {
async computeItemUuidsToFetch(
itemContentSizeDescriptors: ItemContentSizeDescriptor[],
bytesTransferLimit: number,
userUuid: Uuid,
): Promise<{ uuids: Array<string>; transferLimitBreachedBeforeEndOfItems: boolean }> {
const itemUuidsToFetch = []
let totalContentSizeInBytes = 0
@@ -24,6 +26,7 @@ export class ItemTransferCalculator implements ItemTransferCalculatorInterface {
bytesTransferLimit,
itemUuidsToFetch,
itemContentSizeDescriptors,
userUuid,
})
if (transferLimitBreached) {
@@ -41,6 +44,7 @@ export class ItemTransferCalculator implements ItemTransferCalculatorInterface {
async computeItemUuidBundlesToFetch(
itemContentSizeDescriptors: ItemContentSizeDescriptor[],
bytesTransferLimit: number,
userUuid: Uuid,
): Promise<Array<Array<string>>> {
let itemUuidsToFetch = []
let totalContentSizeInBytes = 0
@@ -56,6 +60,7 @@ export class ItemTransferCalculator implements ItemTransferCalculatorInterface {
bytesTransferLimit,
itemUuidsToFetch,
itemContentSizeDescriptors,
userUuid,
})
if (transferLimitBreached) {
@@ -77,15 +82,20 @@ export class ItemTransferCalculator implements ItemTransferCalculatorInterface {
bytesTransferLimit: number
itemUuidsToFetch: Array<string>
itemContentSizeDescriptors: ItemContentSizeDescriptor[]
userUuid: Uuid
}): boolean {
const transferLimitBreached = dto.totalContentSizeInBytes >= dto.bytesTransferLimit
const transferLimitBreachedAtFirstItem =
transferLimitBreached && dto.itemUuidsToFetch.length === 1 && dto.itemContentSizeDescriptors.length > 1
if (transferLimitBreachedAtFirstItem) {
this.logger.warn(
`Item ${dto.itemUuidsToFetch[0]} is breaching the content size transfer limit: ${dto.bytesTransferLimit}`,
)
this.logger.warn('Item is breaching the content size transfer limit at first item in the bundle to fetch.', {
codeTag: 'ItemTransferCalculator',
itemUuid: dto.itemUuidsToFetch[0],
totalContentSizeInBytes: dto.totalContentSizeInBytes,
bytesTransferLimit: dto.bytesTransferLimit,
userId: dto.userUuid.value,
})
}
return transferLimitBreached && !transferLimitBreachedAtFirstItem
@@ -1,12 +1,16 @@
import { Uuid } from '@standardnotes/domain-core'
import { ItemContentSizeDescriptor } from './ItemContentSizeDescriptor'
export interface ItemTransferCalculatorInterface {
computeItemUuidsToFetch(
itemContentSizeDescriptors: ItemContentSizeDescriptor[],
bytesTransferLimit: number,
userUuid: Uuid,
): Promise<{ uuids: Array<string>; transferLimitBreachedBeforeEndOfItems: boolean }>
computeItemUuidBundlesToFetch(
itemContentSizeDescriptors: ItemContentSizeDescriptor[],
bytesTransferLimit: number,
userUuid: Uuid,
): Promise<Array<Array<string>>>
}
@@ -3,16 +3,21 @@ import { MetricsStoreInterface } from '../../../Metrics/MetricsStoreInterface'
import { CheckForTrafficAbuse } from './CheckForTrafficAbuse'
import { MetricsSummary } from '../../../Metrics/MetricsSummary'
import { Metric } from '../../../Metrics/Metric'
import { Logger } from 'winston'
describe('CheckForTrafficAbuse', () => {
let metricsStore: MetricsStoreInterface
let timer: TimerInterface
let timeframeLengthInMinutes: number
let threshold: number
let logger: Logger
const createUseCase = () => new CheckForTrafficAbuse(metricsStore, timer)
const createUseCase = () => new CheckForTrafficAbuse(metricsStore, timer, logger)
beforeEach(() => {
logger = {} as jest.Mocked<Logger>
logger.debug = jest.fn()
const metricsSummary: MetricsSummary = {
sum: 101,
max: 0,
@@ -5,14 +5,21 @@ import { CheckForTrafficAbuseDTO } from './CheckForTrafficAbuseDTO'
import { MetricsStoreInterface } from '../../../Metrics/MetricsStoreInterface'
import { Metric } from '../../../Metrics/Metric'
import { MetricsSummary } from '../../../Metrics/MetricsSummary'
import { Logger } from 'winston'
export class CheckForTrafficAbuse implements UseCaseInterface<MetricsSummary> {
constructor(
private metricsStore: MetricsStoreInterface,
private timer: TimerInterface,
private logger: Logger,
) {}
async execute(dto: CheckForTrafficAbuseDTO): Promise<Result<MetricsSummary>> {
this.logger.debug(`Checking for traffic abuse for metric: ${dto.metricToCheck}.`, {
codeTag: 'CheckForTrafficAbuse',
userId: dto.userUuid,
})
const userUuidOrError = Uuid.create(dto.userUuid)
if (userUuidOrError.isFailed()) {
return Result.fail(userUuidOrError.getError())
@@ -35,8 +42,18 @@ export class CheckForTrafficAbuse implements UseCaseInterface<MetricsSummary> {
to: this.timer.getUTCDate(),
})
this.logger.debug(
`Current traffic abuse metric ${dto.metricToCheck} value in timeframe of ${dto.timeframeLengthInMinutes} minutes is ${metricsSummary.sum}. The threshold is ${dto.threshold}`,
{
codeTag: 'CheckForTrafficAbuse',
userId: dto.userUuid,
},
)
if (metricsSummary.sum > dto.threshold) {
return Result.fail(`Traffic abuse detected for metric: ${metricToCheck.props.name}`)
return Result.fail(
`Traffic abuse detected for metric: ${metricToCheck.props.name}. Usage ${metricsSummary.sum} is greater than threshold ${dto.threshold}`,
)
}
return Result.ok(metricsSummary)
@@ -63,6 +63,7 @@ export class GetItems implements UseCaseInterface<GetItemsResult> {
const { uuids, transferLimitBreachedBeforeEndOfItems } = await this.itemTransferCalculator.computeItemUuidsToFetch(
itemContentSizeDescriptors,
this.contentSizeTransferLimit,
userUuid,
)
let items: Array<Item> = []
if (uuids.length > 0) {
@@ -5,6 +5,7 @@ import { MetricsStoreInterface } from '../../Domain/Metrics/MetricsStoreInterfac
import { Metric } from '../../Domain/Metrics/Metric'
import { Uuid } from '@standardnotes/domain-core'
import { MetricsSummary } from '../../Domain/Metrics/MetricsSummary'
import { Logger } from 'winston'
export class RedisMetricStore implements MetricsStoreInterface {
private readonly METRIC_PREFIX = 'metric'
@@ -13,6 +14,7 @@ export class RedisMetricStore implements MetricsStoreInterface {
constructor(
private redisClient: IORedis.Redis,
private timer: TimerInterface,
private logger: Logger,
) {}
async getUserBasedMetricsSummaryWithinTimeRange(dto: {
@@ -21,8 +23,20 @@ export class RedisMetricStore implements MetricsStoreInterface {
from: Date
to: Date
}): Promise<MetricsSummary> {
this.logger.debug(`Fetching user based metrics summary for ${dto.metricName}.`, {
codeTag: 'RedisMetricStore',
userId: dto.userUuid.value,
from: dto.from.toISOString(),
to: dto.to.toISOString(),
})
const keys = this.getKeysRepresentingMinutesBetweenFromAndTo(dto.from, dto.to)
this.logger.debug(`Fetching user based metrics summary for ${dto.metricName} - keys: ${keys.join(', ')}.`, {
codeTag: 'RedisMetricStore',
userId: dto.userUuid.value,
})
let sum = 0
let max = 0
let min = 0
@@ -32,6 +46,11 @@ export class RedisMetricStore implements MetricsStoreInterface {
keys.map((key) => `${this.METRIC_PER_USER_PREFIX}:${dto.userUuid.value}:${dto.metricName}:${key}`),
)
this.logger.debug(`Fetching user based metrics summary for ${dto.metricName} - values: ${values.join(', ')}.`, {
codeTag: 'RedisMetricStore',
userId: dto.userUuid.value,
})
for (const value of values) {
if (!value) {
continue
@@ -9,12 +9,20 @@ import { SyncItems } from '../../Domain/UseCase/Syncing/SyncItems/SyncItems'
import { ApiVersion } from '../../Domain/Api/ApiVersion'
import { SyncResponseFactoryResolverInterface } from '../../Domain/Item/SyncResponse/SyncResponseFactoryResolverInterface'
import { SyncResponse20200115 } from '../../Domain/Item/SyncResponse/SyncResponse20200115'
import { CheckForTrafficAbuse } from '../../Domain/UseCase/Syncing/CheckForTrafficAbuse/CheckForTrafficAbuse'
import { Metric } from '../../Domain/Metrics/Metric'
export class SyncingServer implements ISyncingServer {
constructor(
private syncItemsUseCase: SyncItems,
private syncResponseFactoryResolver: SyncResponseFactoryResolverInterface,
private mapper: MapperInterface<SyncResponse20200115, SyncResponse>,
protected checkForTrafficAbuse: CheckForTrafficAbuse,
private strictAbuseProtection: boolean,
private itemOperationsAbuseTimeframeLengthInMinutes: number,
private itemOperationsAbuseThreshold: number,
private payloadSizeAbuseThreshold: number,
private payloadSizeAbuseTimeframeLengthInMinutes: number,
private logger: Logger,
) {}
@@ -23,7 +31,62 @@ export class SyncingServer implements ISyncingServer {
callback: grpc.sendUnaryData<SyncResponse>,
): Promise<void> {
try {
this.logger.debug('[SyncingServer] Syncing items via gRPC')
const userUuid = call.metadata.get('x-user-uuid').pop() as string
const checkForItemOperationsAbuseResult = await this.checkForTrafficAbuse.execute({
metricToCheck: Metric.NAMES.ItemOperation,
userUuid,
threshold: this.itemOperationsAbuseThreshold,
timeframeLengthInMinutes: this.itemOperationsAbuseTimeframeLengthInMinutes,
})
if (checkForItemOperationsAbuseResult.isFailed()) {
this.logger.warn(checkForItemOperationsAbuseResult.getError(), {
userId: userUuid,
})
if (this.strictAbuseProtection) {
const metadata = new grpc.Metadata()
metadata.set('x-sync-error-message', checkForItemOperationsAbuseResult.getError())
metadata.set('x-sync-error-response-code', '429')
return callback(
{
code: Status.INVALID_ARGUMENT,
message: checkForItemOperationsAbuseResult.getError(),
name: 'INVALID_ARGUMENT',
metadata,
},
null,
)
}
}
const checkForPayloadSizeAbuseResult = await this.checkForTrafficAbuse.execute({
metricToCheck: Metric.NAMES.ContentSizeUtilized,
userUuid,
threshold: this.payloadSizeAbuseThreshold,
timeframeLengthInMinutes: this.payloadSizeAbuseTimeframeLengthInMinutes,
})
if (checkForPayloadSizeAbuseResult.isFailed()) {
this.logger.warn(checkForPayloadSizeAbuseResult.getError(), {
userId: userUuid,
})
if (this.strictAbuseProtection) {
const metadata = new grpc.Metadata()
metadata.set('x-sync-error-message', checkForPayloadSizeAbuseResult.getError())
metadata.set('x-sync-error-response-code', '429')
return callback(
{
code: Status.INVALID_ARGUMENT,
message: checkForPayloadSizeAbuseResult.getError(),
name: 'INVALID_ARGUMENT',
metadata,
},
null,
)
}
}
const itemHashesRPC = call.request.getItemsList()
const itemHashes: ItemHash[] = []
@@ -41,7 +104,7 @@ export class SyncingServer implements ISyncingServer {
created_at_timestamp: itemHash.hasCreatedAtTimestamp() ? itemHash.getCreatedAtTimestamp() : undefined,
updated_at: itemHash.hasUpdatedAt() ? itemHash.getUpdatedAt() : undefined,
updated_at_timestamp: itemHash.hasUpdatedAtTimestamp() ? itemHash.getUpdatedAtTimestamp() : undefined,
user_uuid: call.metadata.get('userUuid').pop() as string,
user_uuid: userUuid,
key_system_identifier: itemHash.hasKeySystemIdentifier()
? (itemHash.getKeySystemIdentifier() as string)
: null,
@@ -74,7 +137,6 @@ export class SyncingServer implements ISyncingServer {
}
const apiVersion = call.request.hasApiVersion() ? (call.request.getApiVersion() as string) : ApiVersion.v20161215
const userUuid = call.metadata.get('x-user-uuid').pop() as string
const readOnlyAccess = call.metadata.get('x-read-only-access').pop() === 'true'
if (readOnlyAccess) {
this.logger.debug('Syncing with read-only access', {