mirror of
https://github.com/standardnotes/server
synced 2026-01-25 11:01:09 -05:00
Compare commits
20 Commits
@standardn
...
@standardn
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a8448c116 | ||
|
|
d935157ee8 | ||
|
|
9313e6b568 | ||
|
|
8033177f48 | ||
|
|
11011fa15d | ||
|
|
c2e9f3e72b | ||
|
|
f0fb7fd1cd | ||
|
|
15e342fd51 | ||
|
|
dfa7e06f87 | ||
|
|
a9aef5521b | ||
|
|
a628bdc44e | ||
|
|
db6f966045 | ||
|
|
9b602ed405 | ||
|
|
db15457ce4 | ||
|
|
719d8558a3 | ||
|
|
c207c3fc84 | ||
|
|
4bde4758c3 | ||
|
|
5eb957c82a | ||
|
|
0b38617acf | ||
|
|
377d32c449 |
38
.pnp.cjs
generated
38
.pnp.cjs
generated
@@ -2484,14 +2484,14 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
}]\
|
||||
]],\
|
||||
["@standardnotes/api", [\
|
||||
["npm:1.7.2", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-api-npm-1.7.2-e68e7d4e63-bdfc414e6d.zip/node_modules/@standardnotes/api/",\
|
||||
["npm:1.8.1", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-api-npm-1.8.1-15c2e051d4-76c5d1a2d2.zip/node_modules/@standardnotes/api/",\
|
||||
"packageDependencies": [\
|
||||
["@standardnotes/api", "npm:1.7.2"],\
|
||||
["@standardnotes/api", "npm:1.8.1"],\
|
||||
["@standardnotes/common", "workspace:packages/common"],\
|
||||
["@standardnotes/encryption", "npm:1.15.2"],\
|
||||
["@standardnotes/models", "npm:1.18.2"],\
|
||||
["@standardnotes/responses", "npm:1.10.1"],\
|
||||
["@standardnotes/encryption", "npm:1.15.3"],\
|
||||
["@standardnotes/models", "npm:1.18.3"],\
|
||||
["@standardnotes/responses", "npm:1.10.2"],\
|
||||
["@standardnotes/security", "workspace:packages/security"],\
|
||||
["@standardnotes/utils", "npm:1.9.0"],\
|
||||
["reflect-metadata", "npm:0.1.13"]\
|
||||
@@ -2563,7 +2563,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
["@newrelic/winston-enricher", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:4.0.0"],\
|
||||
["@sentry/node", "npm:7.5.0"],\
|
||||
["@standardnotes/analytics", "workspace:packages/analytics"],\
|
||||
["@standardnotes/api", "npm:1.7.2"],\
|
||||
["@standardnotes/api", "npm:1.8.1"],\
|
||||
["@standardnotes/common", "workspace:packages/common"],\
|
||||
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
||||
["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
|
||||
@@ -2688,13 +2688,13 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
}]\
|
||||
]],\
|
||||
["@standardnotes/encryption", [\
|
||||
["npm:1.15.2", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-encryption-npm-1.15.2-ef86a8281d-6e8336f1e7.zip/node_modules/@standardnotes/encryption/",\
|
||||
["npm:1.15.3", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-encryption-npm-1.15.3-3580c52c1f-1a7863299f.zip/node_modules/@standardnotes/encryption/",\
|
||||
"packageDependencies": [\
|
||||
["@standardnotes/encryption", "npm:1.15.2"],\
|
||||
["@standardnotes/encryption", "npm:1.15.3"],\
|
||||
["@standardnotes/common", "workspace:packages/common"],\
|
||||
["@standardnotes/models", "npm:1.18.2"],\
|
||||
["@standardnotes/responses", "npm:1.10.1"],\
|
||||
["@standardnotes/models", "npm:1.18.3"],\
|
||||
["@standardnotes/responses", "npm:1.10.2"],\
|
||||
["@standardnotes/sncrypto-common", "npm:1.11.1"],\
|
||||
["@standardnotes/utils", "npm:1.9.0"],\
|
||||
["reflect-metadata", "npm:0.1.13"]\
|
||||
@@ -2808,13 +2808,13 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
}]\
|
||||
]],\
|
||||
["@standardnotes/models", [\
|
||||
["npm:1.18.2", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-models-npm-1.18.2-56f35bb72d-88180a93e5.zip/node_modules/@standardnotes/models/",\
|
||||
["npm:1.18.3", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-models-npm-1.18.3-6c65a62f30-21830c805f.zip/node_modules/@standardnotes/models/",\
|
||||
"packageDependencies": [\
|
||||
["@standardnotes/models", "npm:1.18.2"],\
|
||||
["@standardnotes/models", "npm:1.18.3"],\
|
||||
["@standardnotes/common", "workspace:packages/common"],\
|
||||
["@standardnotes/features", "npm:1.52.0"],\
|
||||
["@standardnotes/responses", "npm:1.10.1"],\
|
||||
["@standardnotes/responses", "npm:1.10.2"],\
|
||||
["@standardnotes/utils", "npm:1.9.0"],\
|
||||
["lodash", "npm:4.17.21"],\
|
||||
["reflect-metadata", "npm:0.1.13"]\
|
||||
@@ -2851,10 +2851,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
||||
}]\
|
||||
]],\
|
||||
["@standardnotes/responses", [\
|
||||
["npm:1.10.1", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-responses-npm-1.10.1-9f82fff6c1-b84fb3f71c.zip/node_modules/@standardnotes/responses/",\
|
||||
["npm:1.10.2", {\
|
||||
"packageLocation": "./.yarn/cache/@standardnotes-responses-npm-1.10.2-39d2d1f9b5-364724b5c7.zip/node_modules/@standardnotes/responses/",\
|
||||
"packageDependencies": [\
|
||||
["@standardnotes/responses", "npm:1.10.1"],\
|
||||
["@standardnotes/responses", "npm:1.10.2"],\
|
||||
["@standardnotes/common", "workspace:packages/common"],\
|
||||
["@standardnotes/features", "npm:1.52.0"],\
|
||||
["@standardnotes/security", "workspace:packages/security"],\
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/analytics",
|
||||
"version": "1.29.0",
|
||||
"version": "1.29.1",
|
||||
"engines": {
|
||||
"node": ">=14.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -3,6 +3,26 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.20.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.19.6...@standardnotes/api-gateway@1.20.0) (2022-09-21)
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** add creating web socket connection tokens ([8033177](https://github.com/standardnotes/api-gateway/commit/8033177f48dc961194f24fb7daa1073b8b697b74))
|
||||
|
||||
## [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
|
||||
|
||||
## [1.19.3](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.19.2...@standardnotes/api-gateway@1.19.3) (2022-09-15)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -94,7 +94,7 @@ const requestReport = async (
|
||||
StatisticsMeasure.RegistrationLength,
|
||||
StatisticsMeasure.SubscriptionLength,
|
||||
StatisticsMeasure.RegistrationToSubscriptionTime,
|
||||
StatisticsMeasure.SubscriptionCancelToExpireTime,
|
||||
StatisticsMeasure.RemainingSubscriptionTimePercentage,
|
||||
StatisticsMeasure.NotesCountFreeUsers,
|
||||
StatisticsMeasure.NotesCountPaidUsers,
|
||||
StatisticsMeasure.FilesCount,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/api-gateway",
|
||||
"version": "1.19.3",
|
||||
"version": "1.20.0",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -15,6 +15,11 @@ export class WebSocketsController extends BaseHttpController {
|
||||
super()
|
||||
}
|
||||
|
||||
@httpPost('/tokens', TYPES.AuthMiddleware)
|
||||
async createWebSocketConnectionToken(request: Request, response: Response): Promise<void> {
|
||||
await this.httpService.callAuthServer(request, response, 'sockets/tokens', request.body)
|
||||
}
|
||||
|
||||
@httpPost('/', TYPES.AuthMiddleware)
|
||||
async createWebSocketConnection(request: Request, response: Response): Promise<void> {
|
||||
if (!request.headers.connectionid) {
|
||||
|
||||
@@ -66,5 +66,8 @@ SENTRY_ENVIRONMENT=
|
||||
VALET_TOKEN_SECRET=
|
||||
VALET_TOKEN_TTL=
|
||||
|
||||
WEB_SOCKET_CONNECTION_TOKEN_SECRET=
|
||||
WEB_SOCKET_CONNECTION_TOKEN_TTL=
|
||||
|
||||
# (Optional) Analytics
|
||||
ANALYTICS_ENABLED=false
|
||||
|
||||
@@ -3,6 +3,48 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.30.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.30.0...@standardnotes/auth-server@1.30.1) (2022-09-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** missing injectable annotation ([d935157](https://github.com/standardnotes/server/commit/d935157ee8425d427fa52465e766d18e29332b5b))
|
||||
|
||||
# [1.30.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.29.1...@standardnotes/auth-server@1.30.0) (2022-09-21)
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** add creating web socket connection tokens ([8033177](https://github.com/standardnotes/server/commit/8033177f48dc961194f24fb7daa1073b8b697b74))
|
||||
|
||||
## [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
|
||||
|
||||
* **files:** add verifying permitted operation on valet token ([377d32c](https://github.com/standardnotes/server/commit/377d32c4498305f0f59ff59e7357f0d2f10ce3a2))
|
||||
|
||||
## [1.28.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.28.0...@standardnotes/auth-server@1.28.1) (2022-09-15)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -10,7 +10,6 @@ import '../src/Controller/SessionsController'
|
||||
import '../src/Controller/UsersController'
|
||||
import '../src/Controller/SettingsController'
|
||||
import '../src/Controller/FeaturesController'
|
||||
import '../src/Controller/WebSocketsController'
|
||||
import '../src/Controller/AdminController'
|
||||
import '../src/Controller/InternalController'
|
||||
import '../src/Controller/SubscriptionTokensController'
|
||||
@@ -21,6 +20,7 @@ import '../src/Controller/SubscriptionSettingsController'
|
||||
|
||||
import '../src/Infra/InversifyExpressUtils/InversifyExpressAuthController'
|
||||
import '../src/Infra/InversifyExpressUtils/InversifyExpressSubscriptionInvitesController'
|
||||
import '../src/Infra/InversifyExpressUtils/InversifyExpressWebSocketsController'
|
||||
|
||||
import * as cors from 'cors'
|
||||
import { urlencoded, json, Request, Response, NextFunction, RequestHandler, ErrorRequestHandler } from 'express'
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/auth-server",
|
||||
"version": "1.28.1",
|
||||
"version": "1.30.1",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
@@ -34,7 +34,7 @@
|
||||
"@newrelic/winston-enricher": "^4.0.0",
|
||||
"@sentry/node": "^7.3.0",
|
||||
"@standardnotes/analytics": "workspace:*",
|
||||
"@standardnotes/api": "^1.7.2",
|
||||
"@standardnotes/api": "^1.8.1",
|
||||
"@standardnotes/common": "workspace:*",
|
||||
"@standardnotes/domain-events": "workspace:*",
|
||||
"@standardnotes/domain-events-infra": "workspace:*",
|
||||
|
||||
@@ -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'
|
||||
@@ -149,6 +156,7 @@ import {
|
||||
TokenEncoder,
|
||||
TokenEncoderInterface,
|
||||
ValetTokenData,
|
||||
WebSocketConnectionTokenData,
|
||||
} from '@standardnotes/security'
|
||||
import { FileUploadedEventHandler } from '../Domain/Handler/FileUploadedEventHandler'
|
||||
import { CreateValetToken } from '../Domain/UseCase/CreateValetToken/CreateValetToken'
|
||||
@@ -201,6 +209,9 @@ import { PaymentFailedEventHandler } from '../Domain/Handler/PaymentFailedEventH
|
||||
import { PaymentSuccessEventHandler } from '../Domain/Handler/PaymentSuccessEventHandler'
|
||||
import { RefundProcessedEventHandler } from '../Domain/Handler/RefundProcessedEventHandler'
|
||||
import { SubscriptionInvitesController } from '../Controller/SubscriptionInvitesController'
|
||||
import { CreateWebSocketConnectionToken } from '../Domain/UseCase/CreateWebSocketConnectionToken/CreateWebSocketConnectionToken'
|
||||
import { WebSocketsController } from '../Controller/WebSocketsController'
|
||||
import { WebSocketServerInterface } from '@standardnotes/api'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const newrelicFormatter = require('@newrelic/winston-enricher')
|
||||
@@ -264,6 +275,7 @@ export class ContainerConfigLoader {
|
||||
// Controller
|
||||
container.bind<AuthController>(TYPES.AuthController).to(AuthController)
|
||||
container.bind<SubscriptionInvitesController>(TYPES.SubscriptionInvitesController).to(SubscriptionInvitesController)
|
||||
container.bind<WebSocketServerInterface>(TYPES.WebSocketsController).to(WebSocketsController)
|
||||
|
||||
// Repositories
|
||||
container.bind<SessionRepositoryInterface>(TYPES.SessionRepository).to(MySQLSessionRepository)
|
||||
@@ -362,6 +374,12 @@ export class ContainerConfigLoader {
|
||||
container.bind(TYPES.AUTH_JWT_TTL).toConstantValue(+env.get('AUTH_JWT_TTL'))
|
||||
container.bind(TYPES.VALET_TOKEN_SECRET).toConstantValue(env.get('VALET_TOKEN_SECRET', true))
|
||||
container.bind(TYPES.VALET_TOKEN_TTL).toConstantValue(+env.get('VALET_TOKEN_TTL', true))
|
||||
container
|
||||
.bind(TYPES.WEB_SOCKET_CONNECTION_TOKEN_SECRET)
|
||||
.toConstantValue(env.get('WEB_SOCKET_CONNECTION_TOKEN_SECRET', true))
|
||||
container
|
||||
.bind(TYPES.WEB_SOCKET_CONNECTION_TOKEN_TTL)
|
||||
.toConstantValue(+env.get('WEB_SOCKET_CONNECTION_TOKEN_TTL', true))
|
||||
container.bind(TYPES.ENCRYPTION_SERVER_KEY).toConstantValue(env.get('ENCRYPTION_SERVER_KEY'))
|
||||
container.bind(TYPES.ACCESS_TOKEN_AGE).toConstantValue(env.get('ACCESS_TOKEN_AGE'))
|
||||
container.bind(TYPES.REFRESH_TOKEN_AGE).toConstantValue(env.get('REFRESH_TOKEN_AGE'))
|
||||
@@ -441,6 +459,9 @@ export class ContainerConfigLoader {
|
||||
container.bind<GetSubscriptionSetting>(TYPES.GetSubscriptionSetting).to(GetSubscriptionSetting)
|
||||
container.bind<GetUserAnalyticsId>(TYPES.GetUserAnalyticsId).to(GetUserAnalyticsId)
|
||||
container.bind<VerifyPredicate>(TYPES.VerifyPredicate).to(VerifyPredicate)
|
||||
container
|
||||
.bind<CreateWebSocketConnectionToken>(TYPES.CreateWebSocketConnectionToken)
|
||||
.to(CreateWebSocketConnectionToken)
|
||||
|
||||
// Handlers
|
||||
container.bind<UserRegisteredEventHandler>(TYPES.UserRegisteredEventHandler).to(UserRegisteredEventHandler)
|
||||
@@ -525,6 +546,11 @@ export class ContainerConfigLoader {
|
||||
container
|
||||
.bind<TokenEncoderInterface<ValetTokenData>>(TYPES.ValetTokenEncoder)
|
||||
.toConstantValue(new TokenEncoder<ValetTokenData>(container.get(TYPES.VALET_TOKEN_SECRET)))
|
||||
container
|
||||
.bind<TokenEncoderInterface<WebSocketConnectionTokenData>>(TYPES.WebSocketConnectionTokenEncoder)
|
||||
.toConstantValue(
|
||||
new TokenEncoder<WebSocketConnectionTokenData>(container.get(TYPES.WEB_SOCKET_CONNECTION_TOKEN_SECRET)),
|
||||
)
|
||||
container.bind<AuthenticationMethodResolver>(TYPES.AuthenticationMethodResolver).to(AuthenticationMethodResolver)
|
||||
container.bind<DomainEventFactory>(TYPES.DomainEventFactory).to(DomainEventFactory)
|
||||
container.bind<AxiosInstance>(TYPES.HTTPClient).toConstantValue(axios.create())
|
||||
@@ -559,6 +585,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
|
||||
|
||||
@@ -6,6 +6,7 @@ const TYPES = {
|
||||
// Controller
|
||||
AuthController: Symbol.for('AuthController'),
|
||||
SubscriptionInvitesController: Symbol.for('SubscriptionInvitesController'),
|
||||
WebSocketsController: Symbol.for('WebSocketsController'),
|
||||
// Repositories
|
||||
UserRepository: Symbol.for('UserRepository'),
|
||||
SessionRepository: Symbol.for('SessionRepository'),
|
||||
@@ -60,6 +61,8 @@ const TYPES = {
|
||||
AUTH_JWT_TTL: Symbol.for('AUTH_JWT_TTL'),
|
||||
VALET_TOKEN_SECRET: Symbol.for('VALET_TOKEN_SECRET'),
|
||||
VALET_TOKEN_TTL: Symbol.for('VALET_TOKEN_TTL'),
|
||||
WEB_SOCKET_CONNECTION_TOKEN_SECRET: Symbol.for('WEB_SOCKET_CONNECTION_TOKEN_SECRET'),
|
||||
WEB_SOCKET_CONNECTION_TOKEN_TTL: Symbol.for('WEB_SOCKET_CONNECTION_TOKEN_TTL'),
|
||||
ENCRYPTION_SERVER_KEY: Symbol.for('ENCRYPTION_SERVER_KEY'),
|
||||
ACCESS_TOKEN_AGE: Symbol.for('ACCESS_TOKEN_AGE'),
|
||||
REFRESH_TOKEN_AGE: Symbol.for('REFRESH_TOKEN_AGE'),
|
||||
@@ -125,6 +128,7 @@ const TYPES = {
|
||||
GetSubscriptionSetting: Symbol.for('GetSubscriptionSetting'),
|
||||
GetUserAnalyticsId: Symbol.for('GetUserAnalyticsId'),
|
||||
VerifyPredicate: Symbol.for('VerifyPredicate'),
|
||||
CreateWebSocketConnectionToken: Symbol.for('CreateWebSocketConnectionToken'),
|
||||
// Handlers
|
||||
UserRegisteredEventHandler: Symbol.for('UserRegisteredEventHandler'),
|
||||
AccountDeletionRequestedEventHandler: Symbol.for('AccountDeletionRequestedEventHandler'),
|
||||
@@ -166,6 +170,7 @@ const TYPES = {
|
||||
CrossServiceTokenEncoder: Symbol.for('CrossServiceTokenEncoder'),
|
||||
SessionTokenEncoder: Symbol.for('SessionTokenEncoder'),
|
||||
ValetTokenEncoder: Symbol.for('ValetTokenEncoder'),
|
||||
WebSocketConnectionTokenEncoder: Symbol.for('WebSocketConnectionTokenEncoder'),
|
||||
AuthenticationMethodResolver: Symbol.for('AuthenticationMethodResolver'),
|
||||
DomainEventPublisher: Symbol.for('DomainEventPublisher'),
|
||||
DomainEventSubscriberFactory: Symbol.for('DomainEventSubscriberFactory'),
|
||||
@@ -189,6 +194,7 @@ const TYPES = {
|
||||
UserSubscriptionService: Symbol.for('UserSubscriptionService'),
|
||||
AnalyticsStore: Symbol.for('AnalyticsStore'),
|
||||
StatisticsStore: Symbol.for('StatisticsStore'),
|
||||
UuidValidator: Symbol.for('UuidValidator'),
|
||||
}
|
||||
|
||||
export default TYPES
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -11,11 +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()
|
||||
}
|
||||
|
||||
@@ -35,9 +39,23 @@ 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,
|
||||
operation: payload.operation as ValetTokenOperation,
|
||||
resources: payload.resources,
|
||||
})
|
||||
|
||||
|
||||
@@ -1,65 +1,28 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import * as express from 'express'
|
||||
import { results } from 'inversify-express-utils'
|
||||
|
||||
import { AddWebSocketsConnection } from '../Domain/UseCase/AddWebSocketsConnection/AddWebSocketsConnection'
|
||||
|
||||
import { WebSocketsController } from './WebSocketsController'
|
||||
import { RemoveWebSocketsConnection } from '../Domain/UseCase/RemoveWebSocketsConnection/RemoveWebSocketsConnection'
|
||||
import { CreateWebSocketConnectionToken } from '../Domain/UseCase/CreateWebSocketConnectionToken/CreateWebSocketConnectionToken'
|
||||
|
||||
describe('WebSocketsController', () => {
|
||||
let addWebSocketsConnection: AddWebSocketsConnection
|
||||
let removeWebSocketsConnection: RemoveWebSocketsConnection
|
||||
let request: express.Request
|
||||
let response: express.Response
|
||||
let createWebSocketConnectionToken: CreateWebSocketConnectionToken
|
||||
|
||||
const createController = () => new WebSocketsController(addWebSocketsConnection, removeWebSocketsConnection)
|
||||
const createController = () => new WebSocketsController(createWebSocketConnectionToken)
|
||||
|
||||
beforeEach(() => {
|
||||
addWebSocketsConnection = {} as jest.Mocked<AddWebSocketsConnection>
|
||||
addWebSocketsConnection.execute = jest.fn()
|
||||
|
||||
removeWebSocketsConnection = {} as jest.Mocked<RemoveWebSocketsConnection>
|
||||
removeWebSocketsConnection.execute = jest.fn()
|
||||
|
||||
request = {
|
||||
body: {
|
||||
userUuid: '1-2-3',
|
||||
},
|
||||
params: {},
|
||||
headers: {},
|
||||
} as jest.Mocked<express.Request>
|
||||
request.params.connectionId = '2-3-4'
|
||||
|
||||
response = {
|
||||
locals: {},
|
||||
} as jest.Mocked<express.Response>
|
||||
response.locals.user = {
|
||||
uuid: '1-2-3',
|
||||
}
|
||||
createWebSocketConnectionToken = {} as jest.Mocked<CreateWebSocketConnectionToken>
|
||||
createWebSocketConnectionToken.execute = jest.fn().mockReturnValue({ token: 'foobar' })
|
||||
})
|
||||
|
||||
it('should persist an established web sockets connection', async () => {
|
||||
const httpResponse = await createController().storeWebSocketsConnection(request, response)
|
||||
it('should create a web sockets connection token', async () => {
|
||||
const response = await createController().createConnectionToken({ userUuid: '1-2-3' })
|
||||
|
||||
expect(httpResponse).toBeInstanceOf(results.JsonResult)
|
||||
expect((<results.JsonResult>httpResponse).statusCode).toEqual(200)
|
||||
|
||||
expect(addWebSocketsConnection.execute).toHaveBeenCalledWith({
|
||||
userUuid: '1-2-3',
|
||||
connectionId: '2-3-4',
|
||||
expect(response).toEqual({
|
||||
status: 200,
|
||||
data: { token: 'foobar' },
|
||||
})
|
||||
})
|
||||
|
||||
it('should remove a disconnected web sockets connection', async () => {
|
||||
const httpResponse = await createController().deleteWebSocketsConnection(request)
|
||||
|
||||
expect(httpResponse).toBeInstanceOf(results.JsonResult)
|
||||
expect((<results.JsonResult>httpResponse).statusCode).toEqual(200)
|
||||
|
||||
expect(removeWebSocketsConnection.execute).toHaveBeenCalledWith({
|
||||
connectionId: '2-3-4',
|
||||
expect(createWebSocketConnectionToken.execute).toHaveBeenCalledWith({
|
||||
userUuid: '1-2-3',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,45 +1,29 @@
|
||||
import { Request, Response } from 'express'
|
||||
import { inject } from 'inversify'
|
||||
import {
|
||||
BaseHttpController,
|
||||
controller,
|
||||
httpDelete,
|
||||
httpPost,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
results,
|
||||
} from 'inversify-express-utils'
|
||||
HttpStatusCode,
|
||||
WebSocketConnectionTokenRequestParams,
|
||||
WebSocketConnectionTokenResponse,
|
||||
WebSocketServerInterface,
|
||||
} from '@standardnotes/api'
|
||||
import { inject, injectable } from 'inversify'
|
||||
|
||||
import TYPES from '../Bootstrap/Types'
|
||||
import { AddWebSocketsConnection } from '../Domain/UseCase/AddWebSocketsConnection/AddWebSocketsConnection'
|
||||
import { RemoveWebSocketsConnection } from '../Domain/UseCase/RemoveWebSocketsConnection/RemoveWebSocketsConnection'
|
||||
import { CreateWebSocketConnectionToken } from '../Domain/UseCase/CreateWebSocketConnectionToken/CreateWebSocketConnectionToken'
|
||||
|
||||
@controller('/sockets')
|
||||
export class WebSocketsController extends BaseHttpController {
|
||||
@injectable()
|
||||
export class WebSocketsController implements WebSocketServerInterface {
|
||||
constructor(
|
||||
@inject(TYPES.AddWebSocketsConnection) private addWebSocketsConnection: AddWebSocketsConnection,
|
||||
@inject(TYPES.RemoveWebSocketsConnection) private removeWebSocketsConnection: RemoveWebSocketsConnection,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
@inject(TYPES.CreateWebSocketConnectionToken)
|
||||
private createWebSocketConnectionToken: CreateWebSocketConnectionToken,
|
||||
) {}
|
||||
|
||||
@httpPost('/:connectionId', TYPES.ApiGatewayAuthMiddleware)
|
||||
async storeWebSocketsConnection(
|
||||
request: Request,
|
||||
response: Response,
|
||||
): Promise<results.JsonResult | results.BadRequestErrorMessageResult> {
|
||||
await this.addWebSocketsConnection.execute({
|
||||
userUuid: response.locals.user.uuid,
|
||||
connectionId: request.params.connectionId,
|
||||
})
|
||||
async createConnectionToken(
|
||||
params: WebSocketConnectionTokenRequestParams,
|
||||
): Promise<WebSocketConnectionTokenResponse> {
|
||||
const result = await this.createWebSocketConnectionToken.execute({ userUuid: params.userUuid as string })
|
||||
|
||||
return this.json({ success: true })
|
||||
}
|
||||
|
||||
@httpDelete('/:connectionId')
|
||||
async deleteWebSocketsConnection(
|
||||
request: Request,
|
||||
): Promise<results.JsonResult | results.BadRequestErrorMessageResult> {
|
||||
await this.removeWebSocketsConnection.execute({ connectionId: request.params.connectionId })
|
||||
|
||||
return this.json({ success: true })
|
||||
return {
|
||||
status: HttpStatusCode.Success,
|
||||
data: result,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { TokenEncoderInterface, ValetTokenData } from '@standardnotes/security'
|
||||
import { TokenEncoderInterface, ValetTokenData, ValetTokenOperation } from '@standardnotes/security'
|
||||
import { CreateValetToken } from './CreateValetToken'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { UserSubscription } from '../../Subscription/UserSubscription'
|
||||
@@ -70,7 +70,7 @@ describe('CreateValetToken', () => {
|
||||
|
||||
it('should create a read valet token', async () => {
|
||||
const response = await createUseCase().execute({
|
||||
operation: 'read',
|
||||
operation: ValetTokenOperation.Read,
|
||||
userUuid: '1-2-3',
|
||||
resources: [
|
||||
{
|
||||
@@ -92,7 +92,7 @@ describe('CreateValetToken', () => {
|
||||
.mockReturnValue({ regularSubscription: null, sharedSubscription: null })
|
||||
|
||||
const response = await createUseCase().execute({
|
||||
operation: 'read',
|
||||
operation: ValetTokenOperation.Read,
|
||||
userUuid: '1-2-3',
|
||||
resources: [
|
||||
{
|
||||
@@ -117,7 +117,7 @@ describe('CreateValetToken', () => {
|
||||
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(150)
|
||||
|
||||
const response = await createUseCase().execute({
|
||||
operation: 'read',
|
||||
operation: ValetTokenOperation.Read,
|
||||
userUuid: '1-2-3',
|
||||
resources: [
|
||||
{
|
||||
@@ -135,7 +135,7 @@ describe('CreateValetToken', () => {
|
||||
|
||||
it('should not create a write valet token if unencrypted file size has not been provided for a resource', async () => {
|
||||
const response = await createUseCase().execute({
|
||||
operation: 'write',
|
||||
operation: ValetTokenOperation.Write,
|
||||
resources: [
|
||||
{
|
||||
remoteIdentifier: '2-3-4',
|
||||
@@ -152,7 +152,7 @@ describe('CreateValetToken', () => {
|
||||
|
||||
it('should create a write valet token', async () => {
|
||||
const response = await createUseCase().execute({
|
||||
operation: 'write',
|
||||
operation: ValetTokenOperation.Write,
|
||||
resources: [
|
||||
{
|
||||
remoteIdentifier: '2-3-4',
|
||||
@@ -192,7 +192,7 @@ describe('CreateValetToken', () => {
|
||||
.mockReturnValue({ regularSubscription, sharedSubscription })
|
||||
|
||||
const response = await createUseCase().execute({
|
||||
operation: 'write',
|
||||
operation: ValetTokenOperation.Write,
|
||||
resources: [
|
||||
{
|
||||
remoteIdentifier: '2-3-4',
|
||||
@@ -232,7 +232,7 @@ describe('CreateValetToken', () => {
|
||||
.mockReturnValue({ regularSubscription: null, sharedSubscription })
|
||||
|
||||
const response = await createUseCase().execute({
|
||||
operation: 'write',
|
||||
operation: ValetTokenOperation.Write,
|
||||
resources: [
|
||||
{
|
||||
remoteIdentifier: '2-3-4',
|
||||
@@ -252,7 +252,7 @@ describe('CreateValetToken', () => {
|
||||
subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue(null)
|
||||
|
||||
const response = await createUseCase().execute({
|
||||
operation: 'write',
|
||||
operation: ValetTokenOperation.Write,
|
||||
userUuid: '1-2-3',
|
||||
resources: [
|
||||
{
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { CreateValetTokenPayload } from '@standardnotes/responses'
|
||||
import { ValetTokenOperation } from '@standardnotes/security'
|
||||
|
||||
export type CreateValetTokenDTO = CreateValetTokenPayload & {
|
||||
export type CreateValetTokenDTO = {
|
||||
operation: ValetTokenOperation
|
||||
resources: Array<{
|
||||
remoteIdentifier: string
|
||||
unencryptedFileSize?: number
|
||||
}>
|
||||
userUuid: string
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { TokenEncoderInterface, WebSocketConnectionTokenData } from '@standardnotes/security'
|
||||
|
||||
import { CreateWebSocketConnectionToken } from './CreateWebSocketConnectionToken'
|
||||
|
||||
describe('CreateWebSocketConnection', () => {
|
||||
let tokenEncoder: TokenEncoderInterface<WebSocketConnectionTokenData>
|
||||
const tokenTTL = 30
|
||||
|
||||
const createUseCase = () => new CreateWebSocketConnectionToken(tokenEncoder, tokenTTL)
|
||||
|
||||
beforeEach(() => {
|
||||
tokenEncoder = {} as jest.Mocked<TokenEncoderInterface<WebSocketConnectionTokenData>>
|
||||
tokenEncoder.encodeExpirableToken = jest.fn().mockReturnValue('foobar')
|
||||
})
|
||||
|
||||
it('should create a web socket connection token', async () => {
|
||||
const result = await createUseCase().execute({ userUuid: '1-2-3' })
|
||||
|
||||
expect(result.token).toEqual('foobar')
|
||||
|
||||
expect(tokenEncoder.encodeExpirableToken).toHaveBeenCalledWith({ userUuid: '1-2-3' }, 30)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
export type CreateWebSocketConnectionDTO = {
|
||||
userUuid: string
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export type CreateWebSocketConnectionResponse = {
|
||||
token: string
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { TokenEncoderInterface, WebSocketConnectionTokenData } from '@standardnotes/security'
|
||||
import { inject, injectable } from 'inversify'
|
||||
|
||||
import TYPES from '../../../Bootstrap/Types'
|
||||
import { UseCaseInterface } from '../UseCaseInterface'
|
||||
import { CreateWebSocketConnectionDTO } from './CreateWebSocketConnectionDTO'
|
||||
import { CreateWebSocketConnectionResponse } from './CreateWebSocketConnectionResponse'
|
||||
|
||||
@injectable()
|
||||
export class CreateWebSocketConnectionToken implements UseCaseInterface {
|
||||
constructor(
|
||||
@inject(TYPES.WebSocketConnectionTokenEncoder)
|
||||
private tokenEncoder: TokenEncoderInterface<WebSocketConnectionTokenData>,
|
||||
@inject(TYPES.WEB_SOCKET_CONNECTION_TOKEN_TTL) private tokenTTL: number,
|
||||
) {}
|
||||
|
||||
async execute(dto: CreateWebSocketConnectionDTO): Promise<CreateWebSocketConnectionResponse> {
|
||||
const data: WebSocketConnectionTokenData = {
|
||||
userUuid: dto.userUuid,
|
||||
}
|
||||
|
||||
return {
|
||||
token: this.tokenEncoder.encodeExpirableToken(data, this.tokenTTL),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { WebSocketServerInterface } from '@standardnotes/api'
|
||||
import { Request, Response } from 'express'
|
||||
import { inject } from 'inversify'
|
||||
import {
|
||||
BaseHttpController,
|
||||
controller,
|
||||
httpDelete,
|
||||
httpPost,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
results,
|
||||
} from 'inversify-express-utils'
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { AddWebSocketsConnection } from '../../Domain/UseCase/AddWebSocketsConnection/AddWebSocketsConnection'
|
||||
import { RemoveWebSocketsConnection } from '../../Domain/UseCase/RemoveWebSocketsConnection/RemoveWebSocketsConnection'
|
||||
|
||||
@controller('/sockets')
|
||||
export class InversifyExpressWebSocketsController extends BaseHttpController {
|
||||
constructor(
|
||||
@inject(TYPES.AddWebSocketsConnection) private addWebSocketsConnection: AddWebSocketsConnection,
|
||||
@inject(TYPES.RemoveWebSocketsConnection) private removeWebSocketsConnection: RemoveWebSocketsConnection,
|
||||
@inject(TYPES.WebSocketsController) private webSocketsController: WebSocketServerInterface,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
@httpPost('/:connectionId', TYPES.ApiGatewayAuthMiddleware)
|
||||
async storeWebSocketsConnection(
|
||||
request: Request,
|
||||
response: Response,
|
||||
): Promise<results.JsonResult | results.BadRequestErrorMessageResult> {
|
||||
await this.addWebSocketsConnection.execute({
|
||||
userUuid: response.locals.user.uuid,
|
||||
connectionId: request.params.connectionId,
|
||||
})
|
||||
|
||||
return this.json({ success: true })
|
||||
}
|
||||
|
||||
@httpDelete('/:connectionId')
|
||||
async deleteWebSocketsConnection(
|
||||
request: Request,
|
||||
): Promise<results.JsonResult | results.BadRequestErrorMessageResult> {
|
||||
await this.removeWebSocketsConnection.execute({ connectionId: request.params.connectionId })
|
||||
|
||||
return this.json({ success: true })
|
||||
}
|
||||
|
||||
@httpPost('/tokens', TYPES.ApiGatewayAuthMiddleware)
|
||||
async createConnectionToken(_request: Request, response: Response): Promise<results.JsonResult> {
|
||||
const result = await this.webSocketsController.createConnectionToken({
|
||||
userUuid: response.locals.user.uuid,
|
||||
})
|
||||
|
||||
return this.json(result)
|
||||
}
|
||||
}
|
||||
@@ -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', {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/common",
|
||||
"version": "1.32.0",
|
||||
"version": "1.33.0",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
34
packages/common/src/Domain/Validator/UuidValidator.spec.ts
Normal file
34
packages/common/src/Domain/Validator/UuidValidator.spec.ts
Normal 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()
|
||||
}
|
||||
})
|
||||
})
|
||||
10
packages/common/src/Domain/Validator/UuidValidator.ts
Normal file
10
packages/common/src/Domain/Validator/UuidValidator.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface ValidatorInterface<T> {
|
||||
validate(data: T): boolean
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -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.8.12](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.8.11...@standardnotes/domain-events-infra@1.8.12) (2022-09-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||
|
||||
## [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
|
||||
|
||||
## [1.8.9](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.8.8...@standardnotes/domain-events-infra@1.8.9) (2022-09-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/domain-events-infra",
|
||||
"version": "1.8.9",
|
||||
"version": "1.8.12",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -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.
|
||||
|
||||
## [2.60.6](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.60.5...@standardnotes/domain-events@2.60.6) (2022-09-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events
|
||||
|
||||
## [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
|
||||
|
||||
## [2.60.3](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.60.2...@standardnotes/domain-events@2.60.3) (2022-09-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/domain-events",
|
||||
"version": "2.60.3",
|
||||
"version": "2.60.6",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -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.3.17](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.3.16...@standardnotes/event-store@1.3.17) (2022-09-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
## [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
|
||||
|
||||
## [1.3.14](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.3.13...@standardnotes/event-store@1.3.14) (2022-09-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/event-store",
|
||||
"version": "1.3.14",
|
||||
"version": "1.3.17",
|
||||
"description": "Event Store Service",
|
||||
"private": true,
|
||||
"main": "dist/src/index.js",
|
||||
|
||||
@@ -3,6 +3,34 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.6.3](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.6.2...@standardnotes/files-server@1.6.3) (2022-09-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
## [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
|
||||
|
||||
* **files:** add verifying permitted operation on valet token ([377d32c](https://github.com/standardnotes/files/commit/377d32c4498305f0f59ff59e7357f0d2f10ce3a2))
|
||||
|
||||
## [1.5.51](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.5.50...@standardnotes/files-server@1.5.51) (2022-09-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/files-server",
|
||||
"version": "1.5.51",
|
||||
"version": "1.6.3",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -11,6 +11,8 @@ import { FilesController } from './FilesController'
|
||||
import { GetFileMetadata } from '../Domain/UseCase/GetFileMetadata/GetFileMetadata'
|
||||
import { results } from 'inversify-express-utils'
|
||||
import { RemoveFile } from '../Domain/UseCase/RemoveFile/RemoveFile'
|
||||
import { ValetTokenOperation } from '@standardnotes/security'
|
||||
import { BadRequestErrorMessageResult } from 'inversify-express-utils/lib/results'
|
||||
|
||||
describe('FilesController', () => {
|
||||
let uploadFileChunk: UploadFileChunk
|
||||
@@ -75,6 +77,8 @@ describe('FilesController', () => {
|
||||
})
|
||||
|
||||
it('should return a writable stream upon file download', async () => {
|
||||
response.locals.permittedOperation = ValetTokenOperation.Read
|
||||
|
||||
request.headers['range'] = 'bytes=0-'
|
||||
|
||||
const result = (await createController().download(request, response)) as () => Writable
|
||||
@@ -89,7 +93,19 @@ describe('FilesController', () => {
|
||||
expect(result()).toBeInstanceOf(Writable)
|
||||
})
|
||||
|
||||
it('should not allow download on invalid operation in the valet token', async () => {
|
||||
response.locals.permittedOperation = ValetTokenOperation.Write
|
||||
|
||||
request.headers['range'] = 'bytes=0-'
|
||||
|
||||
const result = await createController().download(request, response)
|
||||
|
||||
expect(result).toBeInstanceOf(BadRequestErrorMessageResult)
|
||||
})
|
||||
|
||||
it('should return proper byte range on consecutive calls', async () => {
|
||||
response.locals.permittedOperation = ValetTokenOperation.Read
|
||||
|
||||
request.headers['range'] = 'bytes=0-'
|
||||
;(await createController().download(request, response)) as () => Writable
|
||||
|
||||
@@ -112,6 +128,8 @@ describe('FilesController', () => {
|
||||
})
|
||||
|
||||
it('should return a writable stream with custom chunk size', async () => {
|
||||
response.locals.permittedOperation = ValetTokenOperation.Read
|
||||
|
||||
request.headers['x-chunk-size'] = '50000'
|
||||
request.headers['range'] = 'bytes=0-'
|
||||
|
||||
@@ -128,6 +146,8 @@ describe('FilesController', () => {
|
||||
})
|
||||
|
||||
it('should default to maximum chunk size if custom chunk size is too large', async () => {
|
||||
response.locals.permittedOperation = ValetTokenOperation.Read
|
||||
|
||||
request.headers['x-chunk-size'] = '200000'
|
||||
request.headers['range'] = 'bytes=0-'
|
||||
|
||||
@@ -144,12 +164,16 @@ describe('FilesController', () => {
|
||||
})
|
||||
|
||||
it('should not return a writable stream if bytes range is not provided', async () => {
|
||||
response.locals.permittedOperation = ValetTokenOperation.Read
|
||||
|
||||
const httpResponse = await createController().download(request, response)
|
||||
|
||||
expect(httpResponse).toBeInstanceOf(results.BadRequestErrorMessageResult)
|
||||
})
|
||||
|
||||
it('should not return a writable stream if getting file metadata fails', async () => {
|
||||
response.locals.permittedOperation = ValetTokenOperation.Read
|
||||
|
||||
request.headers['range'] = 'bytes=0-'
|
||||
|
||||
getFileMetadata.execute = jest.fn().mockReturnValue({ success: false, message: 'error' })
|
||||
@@ -160,6 +184,8 @@ describe('FilesController', () => {
|
||||
})
|
||||
|
||||
it('should not return a writable stream if creating download stream fails', async () => {
|
||||
response.locals.permittedOperation = ValetTokenOperation.Read
|
||||
|
||||
request.headers['range'] = 'bytes=0-'
|
||||
|
||||
streamDownloadFile.execute = jest.fn().mockReturnValue({ success: false, message: 'error' })
|
||||
@@ -170,6 +196,8 @@ describe('FilesController', () => {
|
||||
})
|
||||
|
||||
it('should create an upload session', async () => {
|
||||
response.locals.permittedOperation = ValetTokenOperation.Write
|
||||
|
||||
await createController().startUpload(request, response)
|
||||
|
||||
expect(createUploadSession.execute).toHaveBeenCalledWith({
|
||||
@@ -178,7 +206,17 @@ describe('FilesController', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should not create an upload session on invalid operation in the valet token', async () => {
|
||||
response.locals.permittedOperation = ValetTokenOperation.Read
|
||||
|
||||
const result = await createController().startUpload(request, response)
|
||||
|
||||
expect(result).toBeInstanceOf(BadRequestErrorMessageResult)
|
||||
})
|
||||
|
||||
it('should return bad request if upload session could not be created', async () => {
|
||||
response.locals.permittedOperation = ValetTokenOperation.Write
|
||||
|
||||
createUploadSession.execute = jest.fn().mockReturnValue({ success: false })
|
||||
|
||||
const httpResponse = await createController().startUpload(request, response)
|
||||
@@ -188,6 +226,8 @@ describe('FilesController', () => {
|
||||
})
|
||||
|
||||
it('should finish an upload session', async () => {
|
||||
response.locals.permittedOperation = ValetTokenOperation.Write
|
||||
|
||||
await createController().finishUpload(request, response)
|
||||
|
||||
expect(finishUploadSession.execute).toHaveBeenCalledWith({
|
||||
@@ -196,7 +236,17 @@ describe('FilesController', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should not finish an upload session on invalid operation in the valet token', async () => {
|
||||
response.locals.permittedOperation = ValetTokenOperation.Read
|
||||
|
||||
const result = await createController().finishUpload(request, response)
|
||||
|
||||
expect(result).toBeInstanceOf(BadRequestErrorMessageResult)
|
||||
})
|
||||
|
||||
it('should return bad request if upload session could not be finished', async () => {
|
||||
response.locals.permittedOperation = ValetTokenOperation.Write
|
||||
|
||||
finishUploadSession.execute = jest.fn().mockReturnValue({ success: false })
|
||||
|
||||
const httpResponse = await createController().finishUpload(request, response)
|
||||
@@ -206,6 +256,8 @@ describe('FilesController', () => {
|
||||
})
|
||||
|
||||
it('should remove a file', async () => {
|
||||
response.locals.permittedOperation = ValetTokenOperation.Delete
|
||||
|
||||
await createController().remove(request, response)
|
||||
|
||||
expect(removeFile.execute).toHaveBeenCalledWith({
|
||||
@@ -215,6 +267,8 @@ describe('FilesController', () => {
|
||||
})
|
||||
|
||||
it('should return bad request if file removal could not be completed', async () => {
|
||||
response.locals.permittedOperation = ValetTokenOperation.Delete
|
||||
|
||||
removeFile.execute = jest.fn().mockReturnValue({ success: false })
|
||||
|
||||
const httpResponse = await createController().remove(request, response)
|
||||
@@ -223,7 +277,18 @@ describe('FilesController', () => {
|
||||
expect(result.statusCode).toEqual(400)
|
||||
})
|
||||
|
||||
it('should return bad request if file removal is not permitted on valet token', async () => {
|
||||
response.locals.permittedOperation = ValetTokenOperation.Read
|
||||
|
||||
const httpResponse = await createController().remove(request, response)
|
||||
const result = await httpResponse.executeAsync()
|
||||
|
||||
expect(result.statusCode).toEqual(400)
|
||||
})
|
||||
|
||||
it('should upload a chunk to an upload session', async () => {
|
||||
response.locals.permittedOperation = ValetTokenOperation.Write
|
||||
|
||||
request.headers['x-chunk-id'] = '2'
|
||||
request.body = Buffer.from([123])
|
||||
|
||||
@@ -233,11 +298,14 @@ describe('FilesController', () => {
|
||||
chunkId: 2,
|
||||
data: Buffer.from([123]),
|
||||
resourceRemoteIdentifier: '2-3-4',
|
||||
resourceUnencryptedFileSize: 123,
|
||||
userUuid: '1-2-3',
|
||||
})
|
||||
})
|
||||
|
||||
it('should return bad request if chunk could not be uploaded', async () => {
|
||||
response.locals.permittedOperation = ValetTokenOperation.Write
|
||||
|
||||
request.headers['x-chunk-id'] = '2'
|
||||
request.body = Buffer.from([123])
|
||||
uploadFileChunk.execute = jest.fn().mockReturnValue({ success: false })
|
||||
@@ -248,7 +316,18 @@ describe('FilesController', () => {
|
||||
expect(result.statusCode).toEqual(400)
|
||||
})
|
||||
|
||||
it('should return bad request if valet token is not permitted', async () => {
|
||||
response.locals.permittedOperation = ValetTokenOperation.Read
|
||||
|
||||
const httpResponse = await createController().uploadChunk(request, response)
|
||||
const result = await httpResponse.executeAsync()
|
||||
|
||||
expect(result.statusCode).toEqual(400)
|
||||
})
|
||||
|
||||
it('should return bad request if chunk id is missing', async () => {
|
||||
response.locals.permittedOperation = ValetTokenOperation.Write
|
||||
|
||||
request.body = Buffer.from([123])
|
||||
|
||||
const httpResponse = await createController().uploadChunk(request, response)
|
||||
|
||||
@@ -9,6 +9,7 @@ import { CreateUploadSession } from '../Domain/UseCase/CreateUploadSession/Creat
|
||||
import { FinishUploadSession } from '../Domain/UseCase/FinishUploadSession/FinishUploadSession'
|
||||
import { GetFileMetadata } from '../Domain/UseCase/GetFileMetadata/GetFileMetadata'
|
||||
import { RemoveFile } from '../Domain/UseCase/RemoveFile/RemoveFile'
|
||||
import { ValetTokenOperation } from '@standardnotes/security'
|
||||
|
||||
@controller('/v1/files', TYPES.ValetTokenAuthMiddleware)
|
||||
export class FilesController extends BaseHttpController {
|
||||
@@ -29,6 +30,10 @@ export class FilesController extends BaseHttpController {
|
||||
_request: Request,
|
||||
response: Response,
|
||||
): Promise<results.BadRequestErrorMessageResult | results.JsonResult> {
|
||||
if (response.locals.permittedOperation !== ValetTokenOperation.Write) {
|
||||
return this.badRequest('Not permitted for this operation')
|
||||
}
|
||||
|
||||
const result = await this.createUploadSession.execute({
|
||||
userUuid: response.locals.userUuid,
|
||||
resourceRemoteIdentifier: response.locals.permittedResources[0].remoteIdentifier,
|
||||
@@ -46,6 +51,10 @@ export class FilesController extends BaseHttpController {
|
||||
request: Request,
|
||||
response: Response,
|
||||
): Promise<results.BadRequestErrorMessageResult | results.JsonResult> {
|
||||
if (response.locals.permittedOperation !== ValetTokenOperation.Write) {
|
||||
return this.badRequest('Not permitted for this operation')
|
||||
}
|
||||
|
||||
const chunkId = +(request.headers['x-chunk-id'] as string)
|
||||
if (!chunkId) {
|
||||
return this.badRequest('Missing x-chunk-id header in request.')
|
||||
@@ -54,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,
|
||||
})
|
||||
@@ -70,6 +80,10 @@ export class FilesController extends BaseHttpController {
|
||||
_request: Request,
|
||||
response: Response,
|
||||
): Promise<results.BadRequestErrorMessageResult | results.JsonResult> {
|
||||
if (response.locals.permittedOperation !== ValetTokenOperation.Write) {
|
||||
return this.badRequest('Not permitted for this operation')
|
||||
}
|
||||
|
||||
const result = await this.finishUploadSession.execute({
|
||||
userUuid: response.locals.userUuid,
|
||||
resourceRemoteIdentifier: response.locals.permittedResources[0].remoteIdentifier,
|
||||
@@ -89,6 +103,10 @@ export class FilesController extends BaseHttpController {
|
||||
_request: Request,
|
||||
response: Response,
|
||||
): Promise<results.BadRequestErrorMessageResult | results.JsonResult> {
|
||||
if (response.locals.permittedOperation !== ValetTokenOperation.Delete) {
|
||||
return this.badRequest('Not permitted for this operation')
|
||||
}
|
||||
|
||||
const result = await this.removeFile.execute({
|
||||
userUuid: response.locals.userUuid,
|
||||
resourceRemoteIdentifier: response.locals.permittedResources[0].remoteIdentifier,
|
||||
@@ -107,6 +125,10 @@ export class FilesController extends BaseHttpController {
|
||||
request: Request,
|
||||
response: Response,
|
||||
): Promise<results.BadRequestErrorMessageResult | (() => Writable)> {
|
||||
if (response.locals.permittedOperation !== ValetTokenOperation.Read) {
|
||||
return this.badRequest('Not permitted for this operation')
|
||||
}
|
||||
|
||||
const range = request.headers['range']
|
||||
if (!range) {
|
||||
return this.badRequest('File download requires range header to be set.')
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -39,6 +39,7 @@ export class UploadFileChunk implements UseCaseInterface {
|
||||
data: dto.data,
|
||||
chunkId: dto.chunkId,
|
||||
filePath,
|
||||
unencryptedFileSize: dto.resourceUnencryptedFileSize,
|
||||
})
|
||||
|
||||
await this.uploadRepository.storeUploadChunkResult(uploadId, {
|
||||
|
||||
@@ -5,4 +5,5 @@ export type UploadFileChunkDTO = {
|
||||
chunkId: ChunkId
|
||||
userUuid: string
|
||||
resourceRemoteIdentifier: string
|
||||
resourceUnencryptedFileSize: number
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/predicates",
|
||||
"version": "1.4.1",
|
||||
"version": "1.4.2",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -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.10.31](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.10.30...@standardnotes/scheduler-server@1.10.31) (2022-09-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||
|
||||
## [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
|
||||
|
||||
## [1.10.28](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.10.27...@standardnotes/scheduler-server@1.10.28) (2022-09-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/scheduler-server",
|
||||
"version": "1.10.28",
|
||||
"version": "1.10.31",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.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.4.0](https://github.com/standardnotes/server/compare/@standardnotes/security@1.3.3...@standardnotes/security@1.4.0) (2022-09-21)
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** add creating web socket connection tokens ([8033177](https://github.com/standardnotes/server/commit/8033177f48dc961194f24fb7daa1073b8b697b74))
|
||||
|
||||
## [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
|
||||
|
||||
* **files:** add verifying permitted operation on valet token ([377d32c](https://github.com/standardnotes/server/commit/377d32c4498305f0f59ff59e7357f0d2f10ce3a2))
|
||||
|
||||
## [1.3.1](https://github.com/standardnotes/server/compare/@standardnotes/security@1.3.0...@standardnotes/security@1.3.1) (2022-09-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/security
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/security",
|
||||
"version": "1.3.1",
|
||||
"version": "1.4.0",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Uuid } from '@standardnotes/common'
|
||||
|
||||
import { ValetTokenOperation } from './ValetTokenOperation'
|
||||
|
||||
export type ValetTokenData = {
|
||||
userUuid: Uuid
|
||||
sharedSubscriptionUuid: Uuid | undefined
|
||||
regularSubscriptionUuid: Uuid
|
||||
permittedOperation: 'read' | 'write' | 'delete'
|
||||
permittedOperation: ValetTokenOperation
|
||||
permittedResources: Array<{
|
||||
remoteIdentifier: string
|
||||
unencryptedFileSize?: number
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export enum ValetTokenOperation {
|
||||
Read = 'read',
|
||||
Write = 'write',
|
||||
Delete = 'delete',
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { Uuid } from '@standardnotes/common'
|
||||
|
||||
export type WebSocketConnectionTokenData = {
|
||||
userUuid: Uuid
|
||||
}
|
||||
@@ -11,3 +11,5 @@ export * from './Token/OfflineFeaturesTokenData'
|
||||
export * from './Token/OfflineUserTokenData'
|
||||
export * from './Token/SessionTokenData'
|
||||
export * from './Token/ValetTokenData'
|
||||
export * from './Token/ValetTokenOperation'
|
||||
export * from './Token/WebSocketConnectionToken'
|
||||
|
||||
@@ -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.8.8](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.8.7...@standardnotes/syncing-server@1.8.8) (2022-09-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/syncing-server
|
||||
|
||||
## [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
|
||||
|
||||
## [1.8.3](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.8.2...@standardnotes/syncing-server@1.8.3) (2022-09-15)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/syncing-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/syncing-server",
|
||||
"version": "1.8.3",
|
||||
"version": "1.8.8",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
46
yarn.lock
46
yarn.lock
@@ -1803,18 +1803,18 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@standardnotes/api@npm:^1.7.2":
|
||||
version: 1.7.2
|
||||
resolution: "@standardnotes/api@npm:1.7.2"
|
||||
"@standardnotes/api@npm:^1.8.1":
|
||||
version: 1.8.1
|
||||
resolution: "@standardnotes/api@npm:1.8.1"
|
||||
dependencies:
|
||||
"@standardnotes/common": ^1.32.0
|
||||
"@standardnotes/encryption": 1.15.2
|
||||
"@standardnotes/models": 1.18.2
|
||||
"@standardnotes/responses": 1.10.1
|
||||
"@standardnotes/encryption": 1.15.3
|
||||
"@standardnotes/models": 1.18.3
|
||||
"@standardnotes/responses": 1.10.2
|
||||
"@standardnotes/security": ^1.1.0
|
||||
"@standardnotes/utils": 1.9.0
|
||||
reflect-metadata: ^0.1.13
|
||||
checksum: bdfc414e6d01620fd047979255a43eb447afbb69d1bb694015b162ad236431273cd234bba4129d13ba94791271aaff71895d726357491d6ab984c7d5a7a8a3f7
|
||||
checksum: 76c5d1a2d29cf7f407813246febf54fe02c5d7cacedcfd1bf5f6ee6630d847f58cae0b5827fbba1c5c5d5a30e56095833c9eff8b413111f8aae9cc17802ffa63
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -1825,7 +1825,7 @@ __metadata:
|
||||
"@newrelic/winston-enricher": ^4.0.0
|
||||
"@sentry/node": ^7.3.0
|
||||
"@standardnotes/analytics": "workspace:*"
|
||||
"@standardnotes/api": ^1.7.2
|
||||
"@standardnotes/api": ^1.8.1
|
||||
"@standardnotes/common": "workspace:*"
|
||||
"@standardnotes/domain-events": "workspace:*"
|
||||
"@standardnotes/domain-events-infra": "workspace:*"
|
||||
@@ -1951,17 +1951,17 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@standardnotes/encryption@npm:1.15.2":
|
||||
version: 1.15.2
|
||||
resolution: "@standardnotes/encryption@npm:1.15.2"
|
||||
"@standardnotes/encryption@npm:1.15.3":
|
||||
version: 1.15.3
|
||||
resolution: "@standardnotes/encryption@npm:1.15.3"
|
||||
dependencies:
|
||||
"@standardnotes/common": ^1.32.0
|
||||
"@standardnotes/models": 1.18.2
|
||||
"@standardnotes/responses": 1.10.1
|
||||
"@standardnotes/models": 1.18.3
|
||||
"@standardnotes/responses": 1.10.2
|
||||
"@standardnotes/sncrypto-common": 1.11.1
|
||||
"@standardnotes/utils": 1.9.0
|
||||
reflect-metadata: ^0.1.13
|
||||
checksum: 6e8336f1e7e961fbd42c4890458dca877da62dcc1987f7e9a7fb6ca230821276fce6a33652669bcc1752a80ffc55e4cf82b8631f7902d9714f4a07a7956092b0
|
||||
checksum: 1a7863299f86ee28de1640b93277f8b3e206bec2b34a205eb6e6fd6c6899a4908623acd9f2452d71a83c542b1f181408e7743386e5e1079239c6c0fa384242c9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -2066,17 +2066,17 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@standardnotes/models@npm:1.18.2":
|
||||
version: 1.18.2
|
||||
resolution: "@standardnotes/models@npm:1.18.2"
|
||||
"@standardnotes/models@npm:1.18.3":
|
||||
version: 1.18.3
|
||||
resolution: "@standardnotes/models@npm:1.18.3"
|
||||
dependencies:
|
||||
"@standardnotes/common": ^1.32.0
|
||||
"@standardnotes/features": 1.52.0
|
||||
"@standardnotes/responses": 1.10.1
|
||||
"@standardnotes/responses": 1.10.2
|
||||
"@standardnotes/utils": 1.9.0
|
||||
lodash: ^4.17.21
|
||||
reflect-metadata: ^0.1.13
|
||||
checksum: 88180a93e5acdc349e1f96159c40610d7f52d49f0566386d9d6db8767d5ac4ba73af3131c8e433afa253557349e3f96238f6b2060e94df51ceedb5d378b3dd1f
|
||||
checksum: 21830c805ffa1ac2184c64903f88915b7b439eb4eb80ac0686c4920a9a4c86cc6c71a3daeb1ede8f3fe6cf0ce106f7ba396f994165306c1c59c05902a7ec075a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -2105,15 +2105,15 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@standardnotes/responses@npm:1.10.1":
|
||||
version: 1.10.1
|
||||
resolution: "@standardnotes/responses@npm:1.10.1"
|
||||
"@standardnotes/responses@npm:1.10.2":
|
||||
version: 1.10.2
|
||||
resolution: "@standardnotes/responses@npm:1.10.2"
|
||||
dependencies:
|
||||
"@standardnotes/common": ^1.32.0
|
||||
"@standardnotes/features": 1.52.0
|
||||
"@standardnotes/security": ^1.1.0
|
||||
reflect-metadata: ^0.1.13
|
||||
checksum: b84fb3f71cc32286fc757280e01c2da7fd0576e96455bfd53c5e55f807875d7201a23e727a7c702277b90f1959837a9a0cbda94ca6a4f4ad6a4896e306ed851c
|
||||
checksum: 364724b5c7efa06948a240da82320817bce58049d6ea226cc2e828e86144ba4d19e5ae6fa437311438008b16aff7bd8cbe7e3f7475a2e9c89cf47650e1e1e9b0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user