diff --git a/.pnp.cjs b/.pnp.cjs index 437099136..d967e2781 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -126,7 +126,7 @@ const RAW_RUNTIME_STATE = ["@lerna-lite/cli", "npm:1.6.0"],\ ["@lerna-lite/list", "npm:1.6.0"],\ ["@lerna-lite/run", "npm:1.6.0"],\ - ["@sentry/node", "npm:7.19.0"],\ + ["@sentry/node", "npm:7.27.0"],\ ["@types/jest", "npm:29.1.1"],\ ["@types/newrelic", "npm:7.0.4"],\ ["@types/node", "npm:18.11.9"],\ @@ -2438,16 +2438,6 @@ const RAW_RUNTIME_STATE = }]\ ]],\ ["@sentry/core", [\ - ["npm:7.19.0", {\ - "packageLocation": "./.yarn/cache/@sentry-core-npm-7.19.0-151e6173ac-cabd7852ff.zip/node_modules/@sentry/core/",\ - "packageDependencies": [\ - ["@sentry/core", "npm:7.19.0"],\ - ["@sentry/types", "npm:7.19.0"],\ - ["@sentry/utils", "npm:7.19.0"],\ - ["tslib", "npm:1.14.1"]\ - ],\ - "linkType": "HARD"\ - }],\ ["npm:7.27.0", {\ "packageLocation": "./.yarn/cache/@sentry-core-npm-7.27.0-72a2ae90aa-1144287db2.zip/node_modules/@sentry/core/",\ "packageDependencies": [\ @@ -2473,20 +2463,6 @@ const RAW_RUNTIME_STATE = }]\ ]],\ ["@sentry/node", [\ - ["npm:7.19.0", {\ - "packageLocation": "./.yarn/cache/@sentry-node-npm-7.19.0-fd3d8dbde1-3a69647d2e.zip/node_modules/@sentry/node/",\ - "packageDependencies": [\ - ["@sentry/node", "npm:7.19.0"],\ - ["@sentry/core", "npm:7.19.0"],\ - ["@sentry/types", "npm:7.19.0"],\ - ["@sentry/utils", "npm:7.19.0"],\ - ["cookie", "npm:0.4.2"],\ - ["https-proxy-agent", "npm:5.0.1"],\ - ["lru_map", "npm:0.3.3"],\ - ["tslib", "npm:1.14.1"]\ - ],\ - "linkType": "HARD"\ - }],\ ["npm:7.27.0", {\ "packageLocation": "./.yarn/cache/@sentry-node-npm-7.27.0-f1028265b5-b85cd47555.zip/node_modules/@sentry/node/",\ "packageDependencies": [\ @@ -2533,13 +2509,6 @@ const RAW_RUNTIME_STATE = }]\ ]],\ ["@sentry/types", [\ - ["npm:7.19.0", {\ - "packageLocation": "./.yarn/cache/@sentry-types-npm-7.19.0-d6ed1960f2-541e1ef49a.zip/node_modules/@sentry/types/",\ - "packageDependencies": [\ - ["@sentry/types", "npm:7.19.0"]\ - ],\ - "linkType": "HARD"\ - }],\ ["npm:7.27.0", {\ "packageLocation": "./.yarn/cache/@sentry-types-npm-7.27.0-67702fc3e1-20ace8aa51.zip/node_modules/@sentry/types/",\ "packageDependencies": [\ @@ -2549,15 +2518,6 @@ const RAW_RUNTIME_STATE = }]\ ]],\ ["@sentry/utils", [\ - ["npm:7.19.0", {\ - "packageLocation": "./.yarn/cache/@sentry-utils-npm-7.19.0-79844d4d90-50e4f391fe.zip/node_modules/@sentry/utils/",\ - "packageDependencies": [\ - ["@sentry/utils", "npm:7.19.0"],\ - ["@sentry/types", "npm:7.19.0"],\ - ["tslib", "npm:1.14.1"]\ - ],\ - "linkType": "HARD"\ - }],\ ["npm:7.27.0", {\ "packageLocation": "./.yarn/cache/@sentry-utils-npm-7.27.0-1935a93244-760c02397d.zip/node_modules/@sentry/utils/",\ "packageDependencies": [\ @@ -2621,7 +2581,7 @@ const RAW_RUNTIME_STATE = "packageDependencies": [\ ["@standardnotes/analytics", "workspace:packages/analytics"],\ ["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\ - ["@sentry/node", "npm:7.19.0"],\ + ["@sentry/node", "npm:7.27.0"],\ ["@standardnotes/common", "workspace:packages/common"],\ ["@standardnotes/domain-core", "workspace:packages/domain-core"],\ ["@standardnotes/domain-events", "workspace:packages/domain-events"],\ @@ -2673,7 +2633,7 @@ const RAW_RUNTIME_STATE = "packageDependencies": [\ ["@standardnotes/api-gateway", "workspace:packages/api-gateway"],\ ["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\ - ["@sentry/node", "npm:7.19.0"],\ + ["@sentry/node", "npm:7.27.0"],\ ["@standardnotes/common", "workspace:packages/common"],\ ["@standardnotes/domain-events", "workspace:packages/domain-events"],\ ["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\ @@ -2729,7 +2689,9 @@ const RAW_RUNTIME_STATE = "packageDependencies": [\ ["@standardnotes/auth-server", "workspace:packages/auth"],\ ["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\ - ["@sentry/node", "npm:7.19.0"],\ + ["@sentry/node", "npm:7.27.0"],\ + ["@sentry/profiling-node", "npm:0.0.12"],\ + ["@sentry/tracing", "npm:7.27.0"],\ ["@standardnotes/api", "npm:1.19.0"],\ ["@standardnotes/common", "workspace:packages/common"],\ ["@standardnotes/domain-core", "workspace:packages/domain-core"],\ @@ -2953,7 +2915,7 @@ const RAW_RUNTIME_STATE = "packageLocation": "./packages/files/",\ "packageDependencies": [\ ["@standardnotes/files-server", "workspace:packages/files"],\ - ["@sentry/node", "npm:7.19.0"],\ + ["@sentry/node", "npm:7.27.0"],\ ["@standardnotes/common", "workspace:packages/common"],\ ["@standardnotes/config", "npm:2.4.3"],\ ["@standardnotes/domain-events", "workspace:packages/domain-events"],\ @@ -3088,7 +3050,7 @@ const RAW_RUNTIME_STATE = "packageDependencies": [\ ["@standardnotes/revisions-server", "workspace:packages/revisions"],\ ["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\ - ["@sentry/node", "npm:7.19.0"],\ + ["@sentry/node", "npm:7.27.0"],\ ["@standardnotes/api", "npm:1.19.0"],\ ["@standardnotes/common", "workspace:packages/common"],\ ["@standardnotes/domain-core", "workspace:packages/domain-core"],\ @@ -3133,7 +3095,7 @@ const RAW_RUNTIME_STATE = "packageDependencies": [\ ["@standardnotes/scheduler-server", "workspace:packages/scheduler"],\ ["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\ - ["@sentry/node", "npm:7.19.0"],\ + ["@sentry/node", "npm:7.27.0"],\ ["@standardnotes/common", "workspace:packages/common"],\ ["@standardnotes/domain-core", "workspace:packages/domain-core"],\ ["@standardnotes/domain-events", "workspace:packages/domain-events"],\ @@ -3194,7 +3156,7 @@ const RAW_RUNTIME_STATE = ["@lerna-lite/cli", "npm:1.6.0"],\ ["@lerna-lite/list", "npm:1.6.0"],\ ["@lerna-lite/run", "npm:1.6.0"],\ - ["@sentry/node", "npm:7.19.0"],\ + ["@sentry/node", "npm:7.27.0"],\ ["@types/jest", "npm:29.1.1"],\ ["@types/newrelic", "npm:7.0.4"],\ ["@types/node", "npm:18.11.9"],\ @@ -3362,7 +3324,7 @@ const RAW_RUNTIME_STATE = "packageDependencies": [\ ["@standardnotes/websockets-server", "workspace:packages/websockets"],\ ["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\ - ["@sentry/node", "npm:7.19.0"],\ + ["@sentry/node", "npm:7.27.0"],\ ["@standardnotes/api", "npm:1.19.0"],\ ["@standardnotes/common", "workspace:packages/common"],\ ["@standardnotes/domain-events", "workspace:packages/domain-events"],\ @@ -3402,7 +3364,7 @@ const RAW_RUNTIME_STATE = "packageDependencies": [\ ["@standardnotes/workspace-server", "workspace:packages/workspace"],\ ["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\ - ["@sentry/node", "npm:7.19.0"],\ + ["@sentry/node", "npm:7.27.0"],\ ["@standardnotes/api", "npm:1.19.0"],\ ["@standardnotes/common", "workspace:packages/common"],\ ["@standardnotes/domain-core", "workspace:packages/domain-core"],\ diff --git a/.yarn/cache/@sentry-core-npm-7.19.0-151e6173ac-cabd7852ff.zip b/.yarn/cache/@sentry-core-npm-7.19.0-151e6173ac-cabd7852ff.zip deleted file mode 100644 index af864b792..000000000 Binary files a/.yarn/cache/@sentry-core-npm-7.19.0-151e6173ac-cabd7852ff.zip and /dev/null differ diff --git a/.yarn/cache/@sentry-node-npm-7.19.0-fd3d8dbde1-3a69647d2e.zip b/.yarn/cache/@sentry-node-npm-7.19.0-fd3d8dbde1-3a69647d2e.zip deleted file mode 100644 index 816656471..000000000 Binary files a/.yarn/cache/@sentry-node-npm-7.19.0-fd3d8dbde1-3a69647d2e.zip and /dev/null differ diff --git a/.yarn/cache/@sentry-types-npm-7.19.0-d6ed1960f2-541e1ef49a.zip b/.yarn/cache/@sentry-types-npm-7.19.0-d6ed1960f2-541e1ef49a.zip deleted file mode 100644 index d19f0d553..000000000 Binary files a/.yarn/cache/@sentry-types-npm-7.19.0-d6ed1960f2-541e1ef49a.zip and /dev/null differ diff --git a/.yarn/cache/@sentry-utils-npm-7.19.0-79844d4d90-50e4f391fe.zip b/.yarn/cache/@sentry-utils-npm-7.19.0-79844d4d90-50e4f391fe.zip deleted file mode 100644 index 61168fe90..000000000 Binary files a/.yarn/cache/@sentry-utils-npm-7.19.0-79844d4d90-50e4f391fe.zip and /dev/null differ diff --git a/package.json b/package.json index 7221ee6f2..e21bae2fd 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ }, "packageManager": "yarn@4.0.0-rc.25", "dependencies": { - "@sentry/node": "^7.19.0", + "@sentry/node": "^7.27.0", "newrelic": "^9.6.0" } } diff --git a/packages/analytics/package.json b/packages/analytics/package.json index 405f9d91b..01c52fbc4 100644 --- a/packages/analytics/package.json +++ b/packages/analytics/package.json @@ -38,7 +38,7 @@ }, "dependencies": { "@newrelic/winston-enricher": "^4.0.0", - "@sentry/node": "^7.19.0", + "@sentry/node": "^7.27.0", "@standardnotes/common": "workspace:*", "@standardnotes/domain-core": "workspace:^", "@standardnotes/domain-events": "workspace:*", diff --git a/packages/api-gateway/package.json b/packages/api-gateway/package.json index e19c80353..82f18db99 100644 --- a/packages/api-gateway/package.json +++ b/packages/api-gateway/package.json @@ -21,7 +21,7 @@ }, "dependencies": { "@newrelic/winston-enricher": "^4.0.0", - "@sentry/node": "^7.19.0", + "@sentry/node": "^7.27.0", "@standardnotes/common": "workspace:^", "@standardnotes/domain-events": "workspace:*", "@standardnotes/domain-events-infra": "workspace:*", diff --git a/packages/auth/.env.sample b/packages/auth/.env.sample index 399a562ff..ea619c548 100644 --- a/packages/auth/.env.sample +++ b/packages/auth/.env.sample @@ -13,8 +13,8 @@ ENCRYPTION_SERVER_KEY=change-me-! PORT=3000 -DB_HOST=127.0.0.1 -DB_REPLICA_HOST=127.0.0.1 +DB_HOST=localhost +DB_REPLICA_HOST=localhost DB_PORT=3306 DB_USERNAME=auth DB_PASSWORD=changeme123 diff --git a/packages/auth/bin/server.ts b/packages/auth/bin/server.ts index b0d29e34c..1dda364c6 100644 --- a/packages/auth/bin/server.ts +++ b/packages/auth/bin/server.ts @@ -3,6 +3,8 @@ import 'reflect-metadata' import 'newrelic' import * as Sentry from '@sentry/node' +import * as Tracing from '@sentry/tracing' +import { ProfilingIntegration } from '@sentry/profiling-node' import '../src/Controller/HealthCheckController' import '../src/Controller/SessionController' @@ -24,7 +26,7 @@ import '../src/Infra/InversifyExpressUtils/InversifyExpressUserRequestsControlle import '../src/Infra/InversifyExpressUtils/InversifyExpressWebSocketsController' import * as cors from 'cors' -import { urlencoded, json, Request, Response, NextFunction, RequestHandler, ErrorRequestHandler } from 'express' +import { urlencoded, json, Request, Response, NextFunction, ErrorRequestHandler } from 'express' import * as winston from 'winston' import * as dayjs from 'dayjs' import * as utc from 'dayjs/plugin/utc' @@ -53,13 +55,28 @@ void container.load().then((container) => { app.use(cors()) if (env.get('SENTRY_DSN', true)) { + const tracesSampleRate = env.get('SENTRY_TRACE_SAMPLE_RATE', true) + ? +env.get('SENTRY_TRACE_SAMPLE_RATE', true) + : 0 + + const profilesSampleRate = env.get('SENTRY_PROFILES_SAMPLE_RATE', true) + ? +env.get('SENTRY_PROFILES_SAMPLE_RATE', true) + : 0 Sentry.init({ dsn: env.get('SENTRY_DSN'), - integrations: [new Sentry.Integrations.Http({ tracing: false, breadcrumbs: true })], - tracesSampleRate: 0, + integrations: [ + new Sentry.Integrations.Http({ tracing: tracesSampleRate !== 0 }), + new ProfilingIntegration(), + new Tracing.Integrations.Express({ + app, + }), + ], + tracesSampleRate, + profilesSampleRate, }) - app.use(Sentry.Handlers.requestHandler() as RequestHandler) + app.use(Sentry.Handlers.requestHandler()) + app.use(Sentry.Handlers.tracingHandler()) } }) diff --git a/packages/auth/jest.config.js b/packages/auth/jest.config.js index a493ea364..a2109e365 100644 --- a/packages/auth/jest.config.js +++ b/packages/auth/jest.config.js @@ -7,6 +7,6 @@ module.exports = { transform: { ...tsjPreset.transform, }, - coveragePathIgnorePatterns: ['/Bootstrap/', '/InversifyExpressUtils/', '/Infra/', '/Projection/', '/Domain/Email/'], + coveragePathIgnorePatterns: ['/Bootstrap/', '/Infra/', '/Projection/', '/Domain/Email/', '/Mapping/'], setupFilesAfterEnv: ['./test-setup.ts'], } diff --git a/packages/auth/migrations/1671448907955-add_session_traces.ts b/packages/auth/migrations/1671448907955-add_session_traces.ts new file mode 100644 index 000000000..4b94db200 --- /dev/null +++ b/packages/auth/migrations/1671448907955-add_session_traces.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class addSessionTraces1671448907955 implements MigrationInterface { + name = 'addSessionTraces1671448907955' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + 'CREATE TABLE `session_traces` (`uuid` varchar(36) NOT NULL, `user_uuid` varchar(36) NOT NULL, `username` varchar(255) NOT NULL, `subscription_plan_name` varchar(64) NULL, `created_at` datetime NOT NULL, `creation_date` date NOT NULL, `expires_at` datetime NOT NULL, INDEX `subscription_plan_name` (`subscription_plan_name`), INDEX `creation_date` (`creation_date`), PRIMARY KEY (`uuid`)) ENGINE=InnoDB', + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query('DROP INDEX `creation_date` ON `session_traces`') + await queryRunner.query('DROP INDEX `subscription_plan_name` ON `session_traces`') + await queryRunner.query('DROP TABLE `session_traces`') + } +} diff --git a/packages/auth/package.json b/packages/auth/package.json index d177ecaa1..19dc1bd36 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -31,7 +31,9 @@ }, "dependencies": { "@newrelic/winston-enricher": "^4.0.0", - "@sentry/node": "^7.19.0", + "@sentry/node": "^7.27.0", + "@sentry/profiling-node": "^0.0.12", + "@sentry/tracing": "^7.27.0", "@standardnotes/api": "^1.19.0", "@standardnotes/common": "workspace:*", "@standardnotes/domain-core": "workspace:^", diff --git a/packages/auth/src/Bootstrap/Container.ts b/packages/auth/src/Bootstrap/Container.ts index be4757109..f80efb03c 100644 --- a/packages/auth/src/Bootstrap/Container.ts +++ b/packages/auth/src/Bootstrap/Container.ts @@ -194,6 +194,13 @@ import { CreateCrossServiceToken } from '../Domain/UseCase/CreateCrossServiceTok import { ProcessUserRequest } from '../Domain/UseCase/ProcessUserRequest/ProcessUserRequest' import { UserRequestsController } from '../Controller/UserRequestsController' import { EmailSubscriptionUnsubscribedEventHandler } from '../Domain/Handler/EmailSubscriptionUnsubscribedEventHandler' +import { SessionTraceRepositoryInterface } from '../Domain/Session/SessionTraceRepositoryInterface' +import { MySQLSessionTraceRepository } from '../Infra/MySQL/MySQLSessionTraceRepository' +import { MapperInterface } from '@standardnotes/domain-core' +import { SessionTracePersistenceMapper } from '../Mapping/SessionTracePersistenceMapper' +import { SessionTrace } from '../Domain/Session/SessionTrace' +import { TypeORMSessionTrace } from '../Infra/TypeORM/TypeORMSessionTrace' +import { TraceSession } from '../Domain/UseCase/TraceSession/TraceSession' // eslint-disable-next-line @typescript-eslint/no-var-requires const newrelicFormatter = require('@newrelic/winston-enricher') @@ -231,6 +238,8 @@ export class ContainerConfigLoader { }) container.bind(TYPES.Logger).toConstantValue(logger) + container.bind(TYPES.Timer).toConstantValue(new Timer()) + if (env.get('SNS_TOPIC_ARN', true)) { const snsConfig: AWS.SNS.Types.ClientConfiguration = { apiVersion: 'latest', @@ -265,11 +274,47 @@ export class ContainerConfigLoader { container.bind(TYPES.SQS).toConstantValue(new AWS.SQS(sqsConfig)) } + // Mapping + container + .bind>(TYPES.SessionTracePersistenceMapper) + .toConstantValue(new SessionTracePersistenceMapper()) + // Controller container.bind(TYPES.AuthController).to(AuthController) container.bind(TYPES.SubscriptionInvitesController).to(SubscriptionInvitesController) container.bind(TYPES.UserRequestsController).to(UserRequestsController) + // ORM + container + .bind>(TYPES.ORMOfflineSettingRepository) + .toConstantValue(AppDataSource.getRepository(OfflineSetting)) + container + .bind>(TYPES.ORMOfflineUserSubscriptionRepository) + .toConstantValue(AppDataSource.getRepository(OfflineUserSubscription)) + container + .bind>(TYPES.ORMRevokedSessionRepository) + .toConstantValue(AppDataSource.getRepository(RevokedSession)) + container.bind>(TYPES.ORMRoleRepository).toConstantValue(AppDataSource.getRepository(Role)) + container + .bind>(TYPES.ORMSessionRepository) + .toConstantValue(AppDataSource.getRepository(Session)) + container + .bind>(TYPES.ORMSettingRepository) + .toConstantValue(AppDataSource.getRepository(Setting)) + container + .bind>(TYPES.ORMSharedSubscriptionInvitationRepository) + .toConstantValue(AppDataSource.getRepository(SharedSubscriptionInvitation)) + container + .bind>(TYPES.ORMSubscriptionSettingRepository) + .toConstantValue(AppDataSource.getRepository(SubscriptionSetting)) + container.bind>(TYPES.ORMUserRepository).toConstantValue(AppDataSource.getRepository(User)) + container + .bind>(TYPES.ORMUserSubscriptionRepository) + .toConstantValue(AppDataSource.getRepository(UserSubscription)) + container + .bind>(TYPES.ORMSessionTraceRepository) + .toConstantValue(AppDataSource.getRepository(TypeORMSessionTrace)) + // Repositories container.bind(TYPES.SessionRepository).to(MySQLSessionRepository) container.bind(TYPES.RevokedSessionRepository).to(MySQLRevokedSessionRepository) @@ -300,34 +345,14 @@ export class ContainerConfigLoader { .bind(TYPES.SharedSubscriptionInvitationRepository) .to(MySQLSharedSubscriptionInvitationRepository) container.bind(TYPES.PKCERepository).to(RedisPKCERepository) - - // ORM container - .bind>(TYPES.ORMOfflineSettingRepository) - .toConstantValue(AppDataSource.getRepository(OfflineSetting)) - container - .bind>(TYPES.ORMOfflineUserSubscriptionRepository) - .toConstantValue(AppDataSource.getRepository(OfflineUserSubscription)) - container - .bind>(TYPES.ORMRevokedSessionRepository) - .toConstantValue(AppDataSource.getRepository(RevokedSession)) - container.bind>(TYPES.ORMRoleRepository).toConstantValue(AppDataSource.getRepository(Role)) - container - .bind>(TYPES.ORMSessionRepository) - .toConstantValue(AppDataSource.getRepository(Session)) - container - .bind>(TYPES.ORMSettingRepository) - .toConstantValue(AppDataSource.getRepository(Setting)) - container - .bind>(TYPES.ORMSharedSubscriptionInvitationRepository) - .toConstantValue(AppDataSource.getRepository(SharedSubscriptionInvitation)) - container - .bind>(TYPES.ORMSubscriptionSettingRepository) - .toConstantValue(AppDataSource.getRepository(SubscriptionSetting)) - container.bind>(TYPES.ORMUserRepository).toConstantValue(AppDataSource.getRepository(User)) - container - .bind>(TYPES.ORMUserSubscriptionRepository) - .toConstantValue(AppDataSource.getRepository(UserSubscription)) + .bind(TYPES.SessionTraceRepository) + .toConstantValue( + new MySQLSessionTraceRepository( + container.get(TYPES.ORMSessionTraceRepository), + container.get(TYPES.SessionTracePersistenceMapper), + ), + ) // Middleware container.bind(TYPES.AuthMiddleware).to(AuthMiddleware) @@ -383,8 +408,97 @@ export class ContainerConfigLoader { container.bind(TYPES.SYNCING_SERVER_URL).toConstantValue(env.get('SYNCING_SERVER_URL')) container.bind(TYPES.VERSION).toConstantValue(env.get('VERSION')) container.bind(TYPES.PAYMENTS_SERVER_URL).toConstantValue(env.get('PAYMENTS_SERVER_URL', true)) + container + .bind(TYPES.SESSION_TRACE_DAYS_TTL) + .toConstantValue(env.get('SESSION_TRACE_DAYS_TTL', true) ? +env.get('SESSION_TRACE_DAYS_TTL', true) : 90) + + // Services + container.bind(TYPES.DeviceDetector).toConstantValue(new UAParser()) + container.bind(TYPES.SessionService).to(SessionService) + container.bind(TYPES.AuthResponseFactory20161215).to(AuthResponseFactory20161215) + container.bind(TYPES.AuthResponseFactory20190520).to(AuthResponseFactory20190520) + container.bind(TYPES.AuthResponseFactory20200115).to(AuthResponseFactory20200115) + container.bind(TYPES.AuthResponseFactoryResolver).to(AuthResponseFactoryResolver) + container.bind(TYPES.KeyParamsFactory).to(KeyParamsFactory) + container + .bind>(TYPES.SessionTokenDecoder) + .toConstantValue(new TokenDecoder(container.get(TYPES.JWT_SECRET))) + container + .bind>(TYPES.FallbackSessionTokenDecoder) + .toConstantValue(new TokenDecoder(container.get(TYPES.LEGACY_JWT_SECRET))) + container + .bind>(TYPES.CrossServiceTokenDecoder) + .toConstantValue(new TokenDecoder(container.get(TYPES.AUTH_JWT_SECRET))) + container + .bind>(TYPES.OfflineUserTokenDecoder) + .toConstantValue(new TokenDecoder(container.get(TYPES.AUTH_JWT_SECRET))) + container + .bind>(TYPES.WebSocketConnectionTokenDecoder) + .toConstantValue( + new TokenDecoder(container.get(TYPES.WEB_SOCKET_CONNECTION_TOKEN_SECRET)), + ) + container + .bind>(TYPES.OfflineUserTokenEncoder) + .toConstantValue(new TokenEncoder(container.get(TYPES.AUTH_JWT_SECRET))) + container + .bind>(TYPES.SessionTokenEncoder) + .toConstantValue(new TokenEncoder(container.get(TYPES.JWT_SECRET))) + container + .bind>(TYPES.CrossServiceTokenEncoder) + .toConstantValue(new TokenEncoder(container.get(TYPES.AUTH_JWT_SECRET))) + container + .bind>(TYPES.ValetTokenEncoder) + .toConstantValue(new TokenEncoder(container.get(TYPES.VALET_TOKEN_SECRET))) + container.bind(TYPES.AuthenticationMethodResolver).to(AuthenticationMethodResolver) + container.bind(TYPES.DomainEventFactory).to(DomainEventFactory) + container.bind(TYPES.HTTPClient).toConstantValue(axios.create()) + container.bind(TYPES.Crypter).to(CrypterNode) + container.bind(TYPES.SettingService).to(SettingService) + container.bind(TYPES.SubscriptionSettingService).to(SubscriptionSettingService) + container.bind(TYPES.OfflineSettingService).to(OfflineSettingService) + container.bind(TYPES.CryptoNode).toConstantValue(new CryptoNode()) + container.bind(TYPES.ContenDecoder).toConstantValue(new ContentDecoder()) + container.bind(TYPES.WebSocketsClientService).to(WebSocketsClientService) + container.bind(TYPES.RoleService).to(RoleService) + container.bind(TYPES.RoleToSubscriptionMap).to(RoleToSubscriptionMap) + container.bind(TYPES.SettingsAssociationService).to(SettingsAssociationService) + container + .bind(TYPES.SubscriptionSettingsAssociationService) + .to(SubscriptionSettingsAssociationService) + container.bind(TYPES.FeatureService).to(FeatureService) + container.bind(TYPES.SettingInterpreter).to(SettingInterpreter) + container.bind(TYPES.SettingDecrypter).to(SettingDecrypter) + container + .bind>(TYPES.ProtocolVersionSelector) + .toConstantValue(new DeterministicSelector()) + container + .bind>(TYPES.BooleanSelector) + .toConstantValue(new DeterministicSelector()) + container.bind(TYPES.UserSubscriptionService).to(UserSubscriptionService) + container.bind>(TYPES.UuidValidator).toConstantValue(new UuidValidator()) + + if (env.get('SNS_TOPIC_ARN', true)) { + container + .bind(TYPES.DomainEventPublisher) + .toConstantValue(new SNSDomainEventPublisher(container.get(TYPES.SNS), container.get(TYPES.SNS_TOPIC_ARN))) + } else { + container + .bind(TYPES.DomainEventPublisher) + .toConstantValue( + new RedisDomainEventPublisher(container.get(TYPES.Redis), container.get(TYPES.REDIS_EVENTS_CHANNEL)), + ) + } // use cases + container + .bind(TYPES.TraceSession) + .toConstantValue( + new TraceSession( + container.get(TYPES.SessionTraceRepository), + container.get(TYPES.Timer), + container.get(TYPES.SESSION_TRACE_DAYS_TTL), + ), + ) container.bind(TYPES.AuthenticateUser).to(AuthenticateUser) container.bind(TYPES.AuthenticateRequest).to(AuthenticateRequest) container.bind(TYPES.RefreshSessionToken).to(RefreshSessionToken) @@ -483,84 +597,6 @@ export class ContainerConfigLoader { .bind(TYPES.PredicateVerificationRequestedEventHandler) .to(PredicateVerificationRequestedEventHandler) - // Services - container.bind(TYPES.DeviceDetector).toConstantValue(new UAParser()) - container.bind(TYPES.SessionService).to(SessionService) - container.bind(TYPES.AuthResponseFactory20161215).to(AuthResponseFactory20161215) - container.bind(TYPES.AuthResponseFactory20190520).to(AuthResponseFactory20190520) - container.bind(TYPES.AuthResponseFactory20200115).to(AuthResponseFactory20200115) - container.bind(TYPES.AuthResponseFactoryResolver).to(AuthResponseFactoryResolver) - container.bind(TYPES.KeyParamsFactory).to(KeyParamsFactory) - container - .bind>(TYPES.SessionTokenDecoder) - .toConstantValue(new TokenDecoder(container.get(TYPES.JWT_SECRET))) - container - .bind>(TYPES.FallbackSessionTokenDecoder) - .toConstantValue(new TokenDecoder(container.get(TYPES.LEGACY_JWT_SECRET))) - container - .bind>(TYPES.CrossServiceTokenDecoder) - .toConstantValue(new TokenDecoder(container.get(TYPES.AUTH_JWT_SECRET))) - container - .bind>(TYPES.OfflineUserTokenDecoder) - .toConstantValue(new TokenDecoder(container.get(TYPES.AUTH_JWT_SECRET))) - container - .bind>(TYPES.WebSocketConnectionTokenDecoder) - .toConstantValue( - new TokenDecoder(container.get(TYPES.WEB_SOCKET_CONNECTION_TOKEN_SECRET)), - ) - container - .bind>(TYPES.OfflineUserTokenEncoder) - .toConstantValue(new TokenEncoder(container.get(TYPES.AUTH_JWT_SECRET))) - container - .bind>(TYPES.SessionTokenEncoder) - .toConstantValue(new TokenEncoder(container.get(TYPES.JWT_SECRET))) - container - .bind>(TYPES.CrossServiceTokenEncoder) - .toConstantValue(new TokenEncoder(container.get(TYPES.AUTH_JWT_SECRET))) - container - .bind>(TYPES.ValetTokenEncoder) - .toConstantValue(new TokenEncoder(container.get(TYPES.VALET_TOKEN_SECRET))) - container.bind(TYPES.AuthenticationMethodResolver).to(AuthenticationMethodResolver) - container.bind(TYPES.DomainEventFactory).to(DomainEventFactory) - container.bind(TYPES.HTTPClient).toConstantValue(axios.create()) - container.bind(TYPES.Crypter).to(CrypterNode) - container.bind(TYPES.SettingService).to(SettingService) - container.bind(TYPES.SubscriptionSettingService).to(SubscriptionSettingService) - container.bind(TYPES.OfflineSettingService).to(OfflineSettingService) - container.bind(TYPES.CryptoNode).toConstantValue(new CryptoNode()) - container.bind(TYPES.Timer).toConstantValue(new Timer()) - container.bind(TYPES.ContenDecoder).toConstantValue(new ContentDecoder()) - container.bind(TYPES.WebSocketsClientService).to(WebSocketsClientService) - container.bind(TYPES.RoleService).to(RoleService) - container.bind(TYPES.RoleToSubscriptionMap).to(RoleToSubscriptionMap) - container.bind(TYPES.SettingsAssociationService).to(SettingsAssociationService) - container - .bind(TYPES.SubscriptionSettingsAssociationService) - .to(SubscriptionSettingsAssociationService) - container.bind(TYPES.FeatureService).to(FeatureService) - container.bind(TYPES.SettingInterpreter).to(SettingInterpreter) - container.bind(TYPES.SettingDecrypter).to(SettingDecrypter) - container - .bind>(TYPES.ProtocolVersionSelector) - .toConstantValue(new DeterministicSelector()) - container - .bind>(TYPES.BooleanSelector) - .toConstantValue(new DeterministicSelector()) - container.bind(TYPES.UserSubscriptionService).to(UserSubscriptionService) - container.bind>(TYPES.UuidValidator).toConstantValue(new UuidValidator()) - - if (env.get('SNS_TOPIC_ARN', true)) { - container - .bind(TYPES.DomainEventPublisher) - .toConstantValue(new SNSDomainEventPublisher(container.get(TYPES.SNS), container.get(TYPES.SNS_TOPIC_ARN))) - } else { - container - .bind(TYPES.DomainEventPublisher) - .toConstantValue( - new RedisDomainEventPublisher(container.get(TYPES.Redis), container.get(TYPES.REDIS_EVENTS_CHANNEL)), - ) - } - container .bind(TYPES.EmailSubscriptionUnsubscribedEventHandler) .toConstantValue( diff --git a/packages/auth/src/Bootstrap/DataSource.ts b/packages/auth/src/Bootstrap/DataSource.ts index 0aa2165c5..6bf9197c1 100644 --- a/packages/auth/src/Bootstrap/DataSource.ts +++ b/packages/auth/src/Bootstrap/DataSource.ts @@ -10,6 +10,7 @@ import { SharedSubscriptionInvitation } from '../Domain/SharedSubscription/Share import { OfflineUserSubscription } from '../Domain/Subscription/OfflineUserSubscription' import { UserSubscription } from '../Domain/Subscription/UserSubscription' import { User } from '../Domain/User/User' +import { TypeORMSessionTrace } from '../Infra/TypeORM/TypeORMSessionTrace' import { Env } from './Env' const env: Env = new Env() @@ -56,6 +57,7 @@ export const AppDataSource = new DataSource({ OfflineSetting, SharedSubscriptionInvitation, SubscriptionSetting, + TypeORMSessionTrace, ], migrations: [env.get('DB_MIGRATIONS_PATH', true) ?? 'dist/migrations/*.js'], migrationsRun: true, diff --git a/packages/auth/src/Bootstrap/Types.ts b/packages/auth/src/Bootstrap/Types.ts index 6f7408b77..664007281 100644 --- a/packages/auth/src/Bootstrap/Types.ts +++ b/packages/auth/src/Bootstrap/Types.ts @@ -3,6 +3,8 @@ const TYPES = { Redis: Symbol.for('Redis'), SNS: Symbol.for('SNS'), SQS: Symbol.for('SQS'), + // Mapping + SessionTracePersistenceMapper: Symbol.for('SessionTracePersistenceMapper'), // Controller AuthController: Symbol.for('AuthController'), SubscriptionInvitesController: Symbol.for('SubscriptionInvitesController'), @@ -23,6 +25,7 @@ const TYPES = { OfflineSubscriptionTokenRepository: Symbol.for('OfflineSubscriptionTokenRepository'), SharedSubscriptionInvitationRepository: Symbol.for('SharedSubscriptionInvitationRepository'), PKCERepository: Symbol.for('PKCERepository'), + SessionTraceRepository: Symbol.for('SessionTraceRepository'), // ORM ORMOfflineSettingRepository: Symbol.for('ORMOfflineSettingRepository'), ORMOfflineUserSubscriptionRepository: Symbol.for('ORMOfflineUserSubscriptionRepository'), @@ -34,6 +37,7 @@ const TYPES = { ORMSubscriptionSettingRepository: Symbol.for('ORMSubscriptionSettingRepository'), ORMUserRepository: Symbol.for('ORMUserRepository'), ORMUserSubscriptionRepository: Symbol.for('ORMUserSubscriptionRepository'), + ORMSessionTraceRepository: Symbol.for('ORMSessionTraceRepository'), // Middleware AuthMiddleware: Symbol.for('AuthMiddleware'), ApiGatewayAuthMiddleware: Symbol.for('ApiGatewayAuthMiddleware'), @@ -81,6 +85,7 @@ const TYPES = { SYNCING_SERVER_URL: Symbol.for('SYNCING_SERVER_URL'), VERSION: Symbol.for('VERSION'), PAYMENTS_SERVER_URL: Symbol.for('PAYMENTS_SERVER_URL'), + SESSION_TRACE_DAYS_TTL: Symbol.for('SESSION_TRACE_DAYS_TTL'), // use cases AuthenticateUser: Symbol.for('AuthenticateUser'), AuthenticateRequest: Symbol.for('AuthenticateRequest'), @@ -119,6 +124,7 @@ const TYPES = { VerifyPredicate: Symbol.for('VerifyPredicate'), CreateCrossServiceToken: Symbol.for('CreateCrossServiceToken'), ProcessUserRequest: Symbol.for('ProcessUserRequest'), + TraceSession: Symbol.for('TraceSession'), // Handlers UserRegisteredEventHandler: Symbol.for('UserRegisteredEventHandler'), AccountDeletionRequestedEventHandler: Symbol.for('AccountDeletionRequestedEventHandler'), diff --git a/packages/auth/src/Domain/Session/SessionTrace.spec.ts b/packages/auth/src/Domain/Session/SessionTrace.spec.ts new file mode 100644 index 000000000..53a186f61 --- /dev/null +++ b/packages/auth/src/Domain/Session/SessionTrace.spec.ts @@ -0,0 +1,17 @@ +import { SubscriptionPlanName, Username, Uuid } from '@standardnotes/domain-core' +import { SessionTrace } from './SessionTrace' + +describe('SessionTrace', () => { + it('should create an entity', () => { + const entityOrError = SessionTrace.create({ + userUuid: Uuid.create('84c0f8e8-544a-4c7e-9adf-26209303bc1d').getValue(), + username: Username.create('foobar').getValue(), + subscriptionPlanName: SubscriptionPlanName.create(SubscriptionPlanName.NAMES.PlusPlan).getValue(), + createdAt: new Date(), + expiresAt: new Date(), + }) + + expect(entityOrError.isFailed()).toBeFalsy() + expect(entityOrError.getValue().id).not.toBeNull() + }) +}) diff --git a/packages/auth/src/Domain/Session/SessionTrace.ts b/packages/auth/src/Domain/Session/SessionTrace.ts new file mode 100644 index 000000000..550607cc6 --- /dev/null +++ b/packages/auth/src/Domain/Session/SessionTrace.ts @@ -0,0 +1,17 @@ +import { Entity, Result, UniqueEntityId } from '@standardnotes/domain-core' + +import { SessionTraceProps } from './SessionTraceProps' + +export class SessionTrace extends Entity { + get id(): UniqueEntityId { + return this._id + } + + private constructor(props: SessionTraceProps, id?: UniqueEntityId) { + super(props, id) + } + + static create(props: SessionTraceProps, id?: UniqueEntityId): Result { + return Result.ok(new SessionTrace(props, id)) + } +} diff --git a/packages/auth/src/Domain/Session/SessionTraceProps.ts b/packages/auth/src/Domain/Session/SessionTraceProps.ts new file mode 100644 index 000000000..9d7f20bac --- /dev/null +++ b/packages/auth/src/Domain/Session/SessionTraceProps.ts @@ -0,0 +1,9 @@ +import { SubscriptionPlanName, Username, Uuid } from '@standardnotes/domain-core' + +export interface SessionTraceProps { + userUuid: Uuid + username: Username + createdAt: Date + expiresAt: Date + subscriptionPlanName: SubscriptionPlanName | null +} diff --git a/packages/auth/src/Domain/Session/SessionTraceRepositoryInterface.ts b/packages/auth/src/Domain/Session/SessionTraceRepositoryInterface.ts new file mode 100644 index 000000000..51a598d8b --- /dev/null +++ b/packages/auth/src/Domain/Session/SessionTraceRepositoryInterface.ts @@ -0,0 +1,8 @@ +import { Uuid } from '@standardnotes/domain-core' + +import { SessionTrace } from './SessionTrace' + +export interface SessionTraceRepositoryInterface { + save(sessionTrace: SessionTrace): Promise + findOneByUserUuidAndDate(userUuid: Uuid, date: Date): Promise +} diff --git a/packages/auth/src/Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken.spec.ts b/packages/auth/src/Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken.spec.ts index 4f74f6d69..a78371005 100644 --- a/packages/auth/src/Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken.spec.ts +++ b/packages/auth/src/Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken.spec.ts @@ -8,6 +8,10 @@ import { Role } from '../../Role/Role' import { UserRepositoryInterface } from '../../User/UserRepositoryInterface' import { CreateCrossServiceToken } from './CreateCrossServiceToken' +import { RoleToSubscriptionMapInterface } from '../../Role/RoleToSubscriptionMapInterface' +import { TraceSession } from '../TraceSession/TraceSession' +import { Logger } from 'winston' +import { Result, RoleName, SubscriptionPlanName } from '@standardnotes/domain-core' describe('CreateCrossServiceToken', () => { let userProjector: ProjectorInterface @@ -15,6 +19,9 @@ describe('CreateCrossServiceToken', () => { let roleProjector: ProjectorInterface let tokenEncoder: TokenEncoderInterface let userRepository: UserRepositoryInterface + let roleToSubscriptionMap: RoleToSubscriptionMapInterface + let traceSession: TraceSession + let logger: Logger const jwtTTL = 60 let session: Session @@ -22,16 +29,29 @@ describe('CreateCrossServiceToken', () => { let role: Role const createUseCase = () => - new CreateCrossServiceToken(userProjector, sessionProjector, roleProjector, tokenEncoder, userRepository, jwtTTL) + new CreateCrossServiceToken( + userProjector, + sessionProjector, + roleProjector, + tokenEncoder, + userRepository, + jwtTTL, + roleToSubscriptionMap, + traceSession, + logger, + ) beforeEach(() => { session = {} as jest.Mocked - user = {} as jest.Mocked + user = { + uuid: '1-2-3', + email: 'test@test.te', + } as jest.Mocked user.roles = Promise.resolve([role]) userProjector = {} as jest.Mocked> - userProjector.projectSimple = jest.fn().mockReturnValue({ bar: 'baz' }) + userProjector.projectSimple = jest.fn().mockReturnValue({ uuid: '1-2-3', email: 'test@test.te' }) roleProjector = {} as jest.Mocked> roleProjector.projectSimple = jest.fn().mockReturnValue({ name: 'role1', uuid: '1-3-4' }) @@ -45,6 +65,18 @@ describe('CreateCrossServiceToken', () => { userRepository = {} as jest.Mocked userRepository.findOneByUuid = jest.fn().mockReturnValue(user) + + roleToSubscriptionMap = {} as jest.Mocked + roleToSubscriptionMap.filterNonSubscriptionRoles = jest.fn().mockReturnValue([RoleName.NAMES.PlusUser]) + roleToSubscriptionMap.getSubscriptionNameForRoleName = jest + .fn() + .mockReturnValue(SubscriptionPlanName.NAMES.PlusPlan) + + traceSession = {} as jest.Mocked + traceSession.execute = jest.fn() + + logger = {} as jest.Mocked + logger.error = jest.fn() }) it('should create a cross service token for user', async () => { @@ -53,6 +85,11 @@ describe('CreateCrossServiceToken', () => { session, }) + expect(traceSession.execute).toHaveBeenCalledWith({ + userUuid: '1-2-3', + username: 'test@test.te', + subscriptionPlanName: 'PLUS_PLAN', + }) expect(tokenEncoder.encodeExpirableToken).toHaveBeenCalledWith( { roles: [ @@ -65,7 +102,8 @@ describe('CreateCrossServiceToken', () => { test: 'test', }, user: { - bar: 'baz', + email: 'test@test.te', + uuid: '1-2-3', }, }, 60, @@ -86,7 +124,8 @@ describe('CreateCrossServiceToken', () => { }, ], user: { - bar: 'baz', + email: 'test@test.te', + uuid: '1-2-3', }, }, 60, @@ -107,7 +146,8 @@ describe('CreateCrossServiceToken', () => { }, ], user: { - bar: 'baz', + email: 'test@test.te', + uuid: '1-2-3', }, }, 60, @@ -128,4 +168,126 @@ describe('CreateCrossServiceToken', () => { expect(caughtError).not.toBeNull() }) + + it('should trace session without a subscription role', async () => { + roleToSubscriptionMap.filterNonSubscriptionRoles = jest.fn().mockReturnValue([]) + + await createUseCase().execute({ + user, + session, + }) + + expect(traceSession.execute).toHaveBeenCalledWith({ + userUuid: '1-2-3', + username: 'test@test.te', + subscriptionPlanName: null, + }) + expect(tokenEncoder.encodeExpirableToken).toHaveBeenCalledWith( + { + roles: [ + { + name: 'role1', + uuid: '1-3-4', + }, + ], + session: { + test: 'test', + }, + user: { + email: 'test@test.te', + uuid: '1-2-3', + }, + }, + 60, + ) + }) + + it('should trace session without a subscription', async () => { + roleToSubscriptionMap.getSubscriptionNameForRoleName = jest.fn().mockReturnValue(undefined) + + await createUseCase().execute({ + user, + session, + }) + + expect(traceSession.execute).toHaveBeenCalledWith({ + userUuid: '1-2-3', + username: 'test@test.te', + subscriptionPlanName: null, + }) + expect(tokenEncoder.encodeExpirableToken).toHaveBeenCalledWith( + { + roles: [ + { + name: 'role1', + uuid: '1-3-4', + }, + ], + session: { + test: 'test', + }, + user: { + email: 'test@test.te', + uuid: '1-2-3', + }, + }, + 60, + ) + }) + + it('should create token if tracing session throws an error', async () => { + traceSession.execute = jest.fn().mockRejectedValue(new Error('test')) + + await createUseCase().execute({ + user, + session, + }) + + expect(tokenEncoder.encodeExpirableToken).toHaveBeenCalledWith( + { + roles: [ + { + name: 'role1', + uuid: '1-3-4', + }, + ], + session: { + test: 'test', + }, + user: { + email: 'test@test.te', + uuid: '1-2-3', + }, + }, + 60, + ) + }) + + it('should create token if tracing session fails', async () => { + traceSession.execute = jest.fn().mockReturnValue(Result.fail('Ooops')) + + await createUseCase().execute({ + user, + session, + }) + + expect(tokenEncoder.encodeExpirableToken).toHaveBeenCalledWith( + { + roles: [ + { + name: 'role1', + uuid: '1-3-4', + }, + ], + session: { + test: 'test', + }, + user: { + email: 'test@test.te', + uuid: '1-2-3', + }, + }, + 60, + ) + }) }) diff --git a/packages/auth/src/Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken.ts b/packages/auth/src/Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken.ts index b0a37a902..87fec31f2 100644 --- a/packages/auth/src/Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken.ts +++ b/packages/auth/src/Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken.ts @@ -1,14 +1,18 @@ import { RoleName } from '@standardnotes/common' import { TokenEncoderInterface, CrossServiceTokenData } from '@standardnotes/security' - import { inject, injectable } from 'inversify' +import { Logger } from 'winston' + import TYPES from '../../../Bootstrap/Types' import { ProjectorInterface } from '../../../Projection/ProjectorInterface' import { Role } from '../../Role/Role' +import { RoleToSubscriptionMapInterface } from '../../Role/RoleToSubscriptionMapInterface' import { Session } from '../../Session/Session' import { User } from '../../User/User' import { UserRepositoryInterface } from '../../User/UserRepositoryInterface' +import { TraceSession } from '../TraceSession/TraceSession' import { UseCaseInterface } from '../UseCaseInterface' + import { CreateCrossServiceTokenDTO } from './CreateCrossServiceTokenDTO' import { CreateCrossServiceTokenResponse } from './CreateCrossServiceTokenResponse' @@ -21,6 +25,9 @@ export class CreateCrossServiceToken implements UseCaseInterface { @inject(TYPES.CrossServiceTokenEncoder) private tokenEncoder: TokenEncoderInterface, @inject(TYPES.UserRepository) private userRepository: UserRepositoryInterface, @inject(TYPES.AUTH_JWT_TTL) private jwtTTL: number, + @inject(TYPES.RoleToSubscriptionMap) private roleToSubscriptionMap: RoleToSubscriptionMapInterface, + @inject(TYPES.TraceSession) private traceSession: TraceSession, + @inject(TYPES.Logger) private logger: Logger, ) {} async execute(dto: CreateCrossServiceTokenDTO): Promise { @@ -44,6 +51,19 @@ export class CreateCrossServiceToken implements UseCaseInterface { authTokenData.session = this.projectSession(dto.session) } + try { + const traceSessionResult = await this.traceSession.execute({ + userUuid: user.uuid, + username: user.email, + subscriptionPlanName: this.getSubscriptionNameFromRoles(roles), + }) + if (traceSessionResult.isFailed()) { + this.logger.error(traceSessionResult.getError()) + } + } catch (error) { + this.logger.error(`Could not trace session while creating cross service token: ${(error as Error).message}`) + } + return { token: this.tokenEncoder.encodeExpirableToken(authTokenData, this.jwtTTL), } @@ -80,4 +100,17 @@ export class CreateCrossServiceToken implements UseCaseInterface { private projectRoles(roles: Array): Array<{ uuid: string; name: RoleName }> { return roles.map((role) => <{ uuid: string; name: RoleName }>this.roleProjector.projectSimple(role)) } + + private getSubscriptionNameFromRoles(roles: Array): string | null { + const nonSubscriptionRoles = this.roleToSubscriptionMap.filterNonSubscriptionRoles(roles) + if (nonSubscriptionRoles.length === 0) { + return null + } + + const subscriptionName = this.roleToSubscriptionMap.getSubscriptionNameForRoleName( + nonSubscriptionRoles[0].name as RoleName, + ) + + return subscriptionName === undefined ? null : subscriptionName + } } diff --git a/packages/auth/src/Domain/UseCase/TraceSession/TraceSession.spec.ts b/packages/auth/src/Domain/UseCase/TraceSession/TraceSession.spec.ts new file mode 100644 index 000000000..e08ec6fb9 --- /dev/null +++ b/packages/auth/src/Domain/UseCase/TraceSession/TraceSession.spec.ts @@ -0,0 +1,97 @@ +import { Result } from '@standardnotes/domain-core' +import { TimerInterface } from '@standardnotes/time' + +import { SessionTrace } from '../../Session/SessionTrace' +import { SessionTraceRepositoryInterface } from '../../Session/SessionTraceRepositoryInterface' +import { TraceSession } from './TraceSession' + +describe('TraceSession', () => { + let sessionTraceRepository: SessionTraceRepositoryInterface + let timer: TimerInterface + const sessionTraceDaysTTL = 90 + + const createUseCase = () => new TraceSession(sessionTraceRepository, timer, sessionTraceDaysTTL) + + beforeEach(() => { + sessionTraceRepository = {} as jest.Mocked + sessionTraceRepository.findOneByUserUuidAndDate = jest.fn().mockReturnValue(null) + sessionTraceRepository.save = jest.fn() + + timer = {} as jest.Mocked + timer.getUTCDateNDaysAhead = jest.fn().mockReturnValue(new Date()) + }) + + it('should save a session trace', async () => { + const result = await createUseCase().execute({ + userUuid: '0702b137-4f5c-438a-915e-8f8b46572ce5', + username: 'username', + subscriptionPlanName: 'PRO_PLAN', + }) + + expect(result.isFailed()).toBe(false) + expect(result.getValue().props.userUuid.value).toEqual('0702b137-4f5c-438a-915e-8f8b46572ce5') + expect(sessionTraceRepository.save).toHaveBeenCalledTimes(1) + }) + + it('should not save a session trace if one already exists for the same user and date', async () => { + sessionTraceRepository.findOneByUserUuidAndDate = jest.fn().mockReturnValue({} as jest.Mocked) + + const result = await createUseCase().execute({ + userUuid: '0702b137-4f5c-438a-915e-8f8b46572ce5', + username: 'username', + subscriptionPlanName: null, + }) + + expect(result.isFailed()).toBe(false) + expect(sessionTraceRepository.save).not.toHaveBeenCalled() + }) + + it('should return an error if userUuid is invalid', async () => { + const result = await createUseCase().execute({ + userUuid: 'invalid', + username: 'username', + subscriptionPlanName: 'PRO_PLAN', + }) + + expect(result.isFailed()).toBe(true) + expect(sessionTraceRepository.save).not.toHaveBeenCalled() + }) + + it('should return an error if username is invalid', async () => { + const result = await createUseCase().execute({ + userUuid: '0702b137-4f5c-438a-915e-8f8b46572ce5', + username: '', + subscriptionPlanName: 'PRO_PLAN', + }) + + expect(result.isFailed()).toBe(true) + expect(sessionTraceRepository.save).not.toHaveBeenCalled() + }) + + it('should return an error if subscriptionPlanName is invalid', async () => { + const result = await createUseCase().execute({ + userUuid: '0702b137-4f5c-438a-915e-8f8b46572ce5', + username: 'username', + subscriptionPlanName: 'foobar', + }) + + expect(result.isFailed()).toBe(true) + expect(sessionTraceRepository.save).not.toHaveBeenCalled() + }) + + it('should not save a session trace if creating of the session trace fails', async () => { + const mock = jest.spyOn(SessionTrace, 'create') + mock.mockReturnValue(Result.fail('Oops')) + + const result = await createUseCase().execute({ + userUuid: '0702b137-4f5c-438a-915e-8f8b46572ce5', + username: 'username', + subscriptionPlanName: 'PRO_PLAN', + }) + + expect(result.isFailed()).toBe(true) + expect(sessionTraceRepository.save).not.toHaveBeenCalled() + + mock.mockRestore() + }) +}) diff --git a/packages/auth/src/Domain/UseCase/TraceSession/TraceSession.ts b/packages/auth/src/Domain/UseCase/TraceSession/TraceSession.ts new file mode 100644 index 000000000..a79aea6b5 --- /dev/null +++ b/packages/auth/src/Domain/UseCase/TraceSession/TraceSession.ts @@ -0,0 +1,60 @@ +import { Result, SubscriptionPlanName, UseCaseInterface, Username, Uuid } from '@standardnotes/domain-core' +import { TimerInterface } from '@standardnotes/time' + +import { SessionTrace } from '../../Session/SessionTrace' +import { SessionTraceRepositoryInterface } from '../../Session/SessionTraceRepositoryInterface' +import { TraceSessionDTO } from './TraceSessionDTO' + +export class TraceSession implements UseCaseInterface { + constructor( + private sessionTraceRepository: SessionTraceRepositoryInterface, + private timer: TimerInterface, + private sessionTraceDaysTTL: number, + ) {} + + async execute(dto: TraceSessionDTO): Promise> { + const userUuidOrError = Uuid.create(dto.userUuid) + if (userUuidOrError.isFailed()) { + return Result.fail(`Failed to trace session: ${userUuidOrError.getError()}`) + } + const userUuid = userUuidOrError.getValue() + + const usernameOrError = Username.create(dto.username) + if (usernameOrError.isFailed()) { + return Result.fail(`Failed to trace session: ${usernameOrError.getError()}`) + } + const username = usernameOrError.getValue() + + let subscriptionPlanName = null + if (dto.subscriptionPlanName !== null) { + const subscriptionPlanNameOrError = SubscriptionPlanName.create(dto.subscriptionPlanName) + if (subscriptionPlanNameOrError.isFailed()) { + return Result.fail(`Failed to trace session: ${subscriptionPlanNameOrError.getError()}`) + } + subscriptionPlanName = subscriptionPlanNameOrError.getValue() + } + + const alreadyExistingTrace = await this.sessionTraceRepository.findOneByUserUuidAndDate(userUuid, new Date()) + if (alreadyExistingTrace !== null) { + return Result.ok(alreadyExistingTrace) + } + + const expiresAt = this.timer.getUTCDateNDaysAhead(this.sessionTraceDaysTTL) + + const sessionTraceOrError = SessionTrace.create({ + userUuid, + username, + subscriptionPlanName, + createdAt: new Date(), + expiresAt, + }) + if (sessionTraceOrError.isFailed()) { + return Result.fail(`Failed to trace session: ${sessionTraceOrError.getError()}`) + } + const sessionTrace = sessionTraceOrError.getValue() + + await this.sessionTraceRepository.save(sessionTrace) + + return Result.ok(sessionTrace) + } +} diff --git a/packages/auth/src/Domain/UseCase/TraceSession/TraceSessionDTO.ts b/packages/auth/src/Domain/UseCase/TraceSession/TraceSessionDTO.ts new file mode 100644 index 000000000..50feeaf6f --- /dev/null +++ b/packages/auth/src/Domain/UseCase/TraceSession/TraceSessionDTO.ts @@ -0,0 +1,5 @@ +export interface TraceSessionDTO { + userUuid: string + username: string + subscriptionPlanName: string | null +} diff --git a/packages/auth/src/Infra/MySQL/MySQLSessionTraceRepository.ts b/packages/auth/src/Infra/MySQL/MySQLSessionTraceRepository.ts new file mode 100644 index 000000000..0350cbcc8 --- /dev/null +++ b/packages/auth/src/Infra/MySQL/MySQLSessionTraceRepository.ts @@ -0,0 +1,34 @@ +import { MapperInterface, Uuid } from '@standardnotes/domain-core' +import { Repository } from 'typeorm' +import { SessionTrace } from '../../Domain/Session/SessionTrace' +import { SessionTraceRepositoryInterface } from '../../Domain/Session/SessionTraceRepositoryInterface' +import { TypeORMSessionTrace } from '../TypeORM/TypeORMSessionTrace' + +export class MySQLSessionTraceRepository implements SessionTraceRepositoryInterface { + constructor( + private ormRepository: Repository, + private mapper: MapperInterface, + ) {} + + async findOneByUserUuidAndDate(userUuid: Uuid, date: Date): Promise { + const typeOrm = await this.ormRepository + .createQueryBuilder('trace') + .where('trace.user_uuid = :userUuid AND trace.creation_date = :creationDate', { + userUuid: userUuid.value, + creationDate: new Date(date.getFullYear(), date.getMonth(), date.getDate()), + }) + .getOne() + + if (typeOrm === null) { + return null + } + + return this.mapper.toDomain(typeOrm) + } + + async save(sessionTrace: SessionTrace): Promise { + const persistence = this.mapper.toProjection(sessionTrace) + + await this.ormRepository.save(persistence) + } +} diff --git a/packages/auth/src/Infra/TypeORM/TypeORMSessionTrace.ts b/packages/auth/src/Infra/TypeORM/TypeORMSessionTrace.ts new file mode 100644 index 000000000..74ac7ec8d --- /dev/null +++ b/packages/auth/src/Infra/TypeORM/TypeORMSessionTrace.ts @@ -0,0 +1,47 @@ +import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm' + +@Entity({ name: 'session_traces' }) +export class TypeORMSessionTrace { + @PrimaryGeneratedColumn('uuid') + declare uuid: string + + @Column({ + name: 'user_uuid', + length: 36, + }) + declare userUuid: string + + @Column({ + name: 'username', + length: 255, + }) + declare username: string + + @Column({ + name: 'subscription_plan_name', + length: 64, + type: 'varchar', + nullable: true, + }) + @Index('subscription_plan_name') + declare subscriptionPlanName: string | null + + @Column({ + name: 'created_at', + type: 'datetime', + }) + declare createdAt: Date + + @Column({ + name: 'creation_date', + type: 'date', + }) + @Index('creation_date') + declare creationDate: Date + + @Column({ + name: 'expires_at', + type: 'datetime', + }) + declare expiresAt: Date +} diff --git a/packages/auth/src/Mapping/SessionTracePersistenceMapper.ts b/packages/auth/src/Mapping/SessionTracePersistenceMapper.ts new file mode 100644 index 000000000..6c14c6bf4 --- /dev/null +++ b/packages/auth/src/Mapping/SessionTracePersistenceMapper.ts @@ -0,0 +1,62 @@ +import { MapperInterface, SubscriptionPlanName, UniqueEntityId, Username, Uuid } from '@standardnotes/domain-core' +import { SessionTrace } from '../Domain/Session/SessionTrace' +import { TypeORMSessionTrace } from '../Infra/TypeORM/TypeORMSessionTrace' + +export class SessionTracePersistenceMapper implements MapperInterface { + toDomain(projection: TypeORMSessionTrace): SessionTrace { + const userUuidOrError = Uuid.create(projection.userUuid) + if (userUuidOrError.isFailed()) { + throw new Error('Failed to create Uuid from persistence.') + } + const userUuid = userUuidOrError.getValue() + + const usernameOrError = Username.create(projection.username) + if (usernameOrError.isFailed()) { + throw new Error('Failed to create Username from persistence.') + } + const username = usernameOrError.getValue() + + let subscriptionPlanName = null + if (projection.subscriptionPlanName !== null) { + const subscriptionPlanNameOrError = SubscriptionPlanName.create(projection.subscriptionPlanName) + if (subscriptionPlanNameOrError.isFailed()) { + throw new Error('Failed to create SubscriptionPlanName from persistence.') + } + subscriptionPlanName = subscriptionPlanNameOrError.getValue() + } + + const sessionTraceOrError = SessionTrace.create( + { + userUuid, + username, + subscriptionPlanName, + createdAt: projection.createdAt, + expiresAt: projection.expiresAt, + }, + new UniqueEntityId(projection.uuid), + ) + if (sessionTraceOrError.isFailed()) { + throw new Error('Failed to create SessionTrace from persistence.') + } + + return sessionTraceOrError.getValue() + } + + toProjection(domain: SessionTrace): TypeORMSessionTrace { + const typeOrm = new TypeORMSessionTrace() + + typeOrm.uuid = domain.id.toString() + typeOrm.userUuid = domain.props.userUuid.value + typeOrm.username = domain.props.username.value + typeOrm.subscriptionPlanName = domain.props.subscriptionPlanName ? domain.props.subscriptionPlanName.value : null + typeOrm.createdAt = domain.props.createdAt + typeOrm.creationDate = new Date( + domain.props.createdAt.getFullYear(), + domain.props.createdAt.getMonth(), + domain.props.createdAt.getDate(), + ) + typeOrm.expiresAt = domain.props.expiresAt + + return typeOrm + } +} diff --git a/packages/files/package.json b/packages/files/package.json index b21f08738..f38c96c53 100644 --- a/packages/files/package.json +++ b/packages/files/package.json @@ -25,7 +25,7 @@ "upgrade:snjs": "yarn ncu -u '@standardnotes/*'" }, "dependencies": { - "@sentry/node": "^7.19.0", + "@sentry/node": "^7.27.0", "@standardnotes/common": "workspace:*", "@standardnotes/domain-events": "workspace:*", "@standardnotes/domain-events-infra": "workspace:*", diff --git a/packages/revisions/package.json b/packages/revisions/package.json index 48c4f21a2..a3c0e7b3f 100644 --- a/packages/revisions/package.json +++ b/packages/revisions/package.json @@ -24,7 +24,7 @@ }, "dependencies": { "@newrelic/winston-enricher": "^4.0.0", - "@sentry/node": "^7.19.0", + "@sentry/node": "^7.27.0", "@standardnotes/api": "^1.19.0", "@standardnotes/common": "workspace:^", "@standardnotes/domain-core": "workspace:^", diff --git a/packages/scheduler/package.json b/packages/scheduler/package.json index 22ea98fde..53358c670 100644 --- a/packages/scheduler/package.json +++ b/packages/scheduler/package.json @@ -25,7 +25,7 @@ }, "dependencies": { "@newrelic/winston-enricher": "^4.0.0", - "@sentry/node": "^7.19.0", + "@sentry/node": "^7.27.0", "@standardnotes/common": "workspace:*", "@standardnotes/domain-core": "workspace:^", "@standardnotes/domain-events": "workspace:*", diff --git a/packages/syncing-server/bin/server.ts b/packages/syncing-server/bin/server.ts index 1b5e3dabf..dffcb445e 100644 --- a/packages/syncing-server/bin/server.ts +++ b/packages/syncing-server/bin/server.ts @@ -3,7 +3,7 @@ import 'reflect-metadata' import 'newrelic' import * as Sentry from '@sentry/node' -import '@sentry/tracing' +import * as Tracing from '@sentry/tracing' import { ProfilingIntegration } from '@sentry/profiling-node' import '../src/Controller/HealthCheckController' @@ -12,7 +12,7 @@ import '../src/Controller/ItemsController' import helmet from 'helmet' import * as cors from 'cors' -import { urlencoded, json, Request, Response, NextFunction, RequestHandler, ErrorRequestHandler } from 'express' +import { urlencoded, json, Request, Response, NextFunction, ErrorRequestHandler } from 'express' import * as winston from 'winston' import { InversifyExpressServer } from 'inversify-express-utils' @@ -68,12 +68,19 @@ void container.load().then((container) => { : 0 Sentry.init({ dsn: env.get('SENTRY_DSN'), - integrations: [new Sentry.Integrations.Http({ tracing: false, breadcrumbs: true }), new ProfilingIntegration()], + integrations: [ + new Sentry.Integrations.Http({ tracing: tracesSampleRate !== 0 }), + new ProfilingIntegration(), + new Tracing.Integrations.Express({ + app, + }), + ], tracesSampleRate, profilesSampleRate, }) - app.use(Sentry.Handlers.requestHandler() as RequestHandler) + app.use(Sentry.Handlers.requestHandler()) + app.use(Sentry.Handlers.tracingHandler()) } }) diff --git a/packages/websockets/package.json b/packages/websockets/package.json index c146c38ea..5ec0d4888 100644 --- a/packages/websockets/package.json +++ b/packages/websockets/package.json @@ -23,7 +23,7 @@ }, "dependencies": { "@newrelic/winston-enricher": "^4.0.0", - "@sentry/node": "^7.19.0", + "@sentry/node": "^7.27.0", "@standardnotes/api": "^1.19.0", "@standardnotes/common": "workspace:^", "@standardnotes/domain-events": "workspace:^", diff --git a/packages/workspace/package.json b/packages/workspace/package.json index 893118deb..f869527f2 100644 --- a/packages/workspace/package.json +++ b/packages/workspace/package.json @@ -23,7 +23,7 @@ }, "dependencies": { "@newrelic/winston-enricher": "^4.0.0", - "@sentry/node": "^7.19.0", + "@sentry/node": "^7.27.0", "@standardnotes/api": "^1.19.0", "@standardnotes/common": "workspace:*", "@standardnotes/domain-core": "workspace:^", diff --git a/yarn.lock b/yarn.lock index ca8534a1b..d29be7d4a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1706,17 +1706,6 @@ __metadata: languageName: node linkType: hard -"@sentry/core@npm:7.19.0": - version: 7.19.0 - resolution: "@sentry/core@npm:7.19.0" - dependencies: - "@sentry/types": "npm:7.19.0" - "@sentry/utils": "npm:7.19.0" - tslib: "npm:^1.9.3" - checksum: cabd7852ff9ea58287724678f232588f15d63437c7d0bad91543c91c821ac0f19a00835dd986645aed9d9094b2c0a8c82c1e9e4fb43342b9d31c6890010b2ec2 - languageName: node - linkType: hard - "@sentry/core@npm:7.27.0": version: 7.27.0 resolution: "@sentry/core@npm:7.27.0" @@ -1755,21 +1744,6 @@ __metadata: languageName: node linkType: hard -"@sentry/node@npm:^7.19.0": - version: 7.19.0 - resolution: "@sentry/node@npm:7.19.0" - dependencies: - "@sentry/core": "npm:7.19.0" - "@sentry/types": "npm:7.19.0" - "@sentry/utils": "npm:7.19.0" - cookie: "npm:^0.4.1" - https-proxy-agent: "npm:^5.0.0" - lru_map: "npm:^0.3.3" - tslib: "npm:^1.9.3" - checksum: 3a69647d2ea2a8583bb77f3e30aa7cc587568b8e2f02d8ae7cf0c8928f03e4135fc3b9fcbdc90b35f190cb8973d70590fa7e4ef683d43e7e4a4fa50d22ac568a - languageName: node - linkType: hard - "@sentry/profiling-node@npm:^0.0.12": version: 0.0.12 resolution: "@sentry/profiling-node@npm:0.0.12" @@ -1798,13 +1772,6 @@ __metadata: languageName: node linkType: hard -"@sentry/types@npm:7.19.0": - version: 7.19.0 - resolution: "@sentry/types@npm:7.19.0" - checksum: 541e1ef49ae4482bbec605c3a2075a669930db2f707fafa431174010fcd0f2ba57637399822fbe0f4c5750696fe57e450d66c0182c1b7a0a3f4e6be9d030ed55 - languageName: node - linkType: hard - "@sentry/types@npm:7.27.0, @sentry/types@npm:^7.16.0": version: 7.27.0 resolution: "@sentry/types@npm:7.27.0" @@ -1812,16 +1779,6 @@ __metadata: languageName: node linkType: hard -"@sentry/utils@npm:7.19.0": - version: 7.19.0 - resolution: "@sentry/utils@npm:7.19.0" - dependencies: - "@sentry/types": "npm:7.19.0" - tslib: "npm:^1.9.3" - checksum: 50e4f391fe9e6009f417c54a8ec96ba25ae77615d92c7f8c22a496901b9e3f182b78cecbc412bff0b2853812258642962b1ee38f42f9708e089756b4ebb3790e - languageName: node - linkType: hard - "@sentry/utils@npm:7.27.0, @sentry/utils@npm:^7.16.0": version: 7.27.0 resolution: "@sentry/utils@npm:7.27.0" @@ -1876,7 +1833,7 @@ __metadata: resolution: "@standardnotes/analytics@workspace:packages/analytics" dependencies: "@newrelic/winston-enricher": "npm:^4.0.0" - "@sentry/node": "npm:^7.19.0" + "@sentry/node": "npm:^7.27.0" "@standardnotes/common": "workspace:*" "@standardnotes/domain-core": "workspace:^" "@standardnotes/domain-events": "workspace:*" @@ -1910,7 +1867,7 @@ __metadata: resolution: "@standardnotes/api-gateway@workspace:packages/api-gateway" dependencies: "@newrelic/winston-enricher": "npm:^4.0.0" - "@sentry/node": "npm:^7.19.0" + "@sentry/node": "npm:^7.27.0" "@standardnotes/common": "workspace:^" "@standardnotes/domain-events": "workspace:*" "@standardnotes/domain-events-infra": "workspace:*" @@ -1968,7 +1925,9 @@ __metadata: resolution: "@standardnotes/auth-server@workspace:packages/auth" dependencies: "@newrelic/winston-enricher": "npm:^4.0.0" - "@sentry/node": "npm:^7.19.0" + "@sentry/node": "npm:^7.27.0" + "@sentry/profiling-node": "npm:^0.0.12" + "@sentry/tracing": "npm:^7.27.0" "@standardnotes/api": "npm:^1.19.0" "@standardnotes/common": "workspace:*" "@standardnotes/domain-core": "workspace:^" @@ -2188,7 +2147,7 @@ __metadata: version: 0.0.0-use.local resolution: "@standardnotes/files-server@workspace:packages/files" dependencies: - "@sentry/node": "npm:^7.19.0" + "@sentry/node": "npm:^7.27.0" "@standardnotes/common": "workspace:*" "@standardnotes/config": "npm:2.4.3" "@standardnotes/domain-events": "workspace:*" @@ -2318,7 +2277,7 @@ __metadata: resolution: "@standardnotes/revisions-server@workspace:packages/revisions" dependencies: "@newrelic/winston-enricher": "npm:^4.0.0" - "@sentry/node": "npm:^7.19.0" + "@sentry/node": "npm:^7.27.0" "@standardnotes/api": "npm:^1.19.0" "@standardnotes/common": "workspace:^" "@standardnotes/domain-core": "workspace:^" @@ -2361,7 +2320,7 @@ __metadata: resolution: "@standardnotes/scheduler-server@workspace:packages/scheduler" dependencies: "@newrelic/winston-enricher": "npm:^4.0.0" - "@sentry/node": "npm:^7.19.0" + "@sentry/node": "npm:^7.27.0" "@standardnotes/common": "workspace:*" "@standardnotes/domain-core": "workspace:^" "@standardnotes/domain-events": "workspace:*" @@ -2418,7 +2377,7 @@ __metadata: "@lerna-lite/cli": "npm:^1.5.1" "@lerna-lite/list": "npm:^1.5.1" "@lerna-lite/run": "npm:^1.5.1" - "@sentry/node": "npm:^7.19.0" + "@sentry/node": "npm:^7.27.0" "@types/jest": "npm:^29.1.1" "@types/newrelic": "npm:^7.0.4" "@types/node": "npm:^18.11.9" @@ -2575,7 +2534,7 @@ __metadata: resolution: "@standardnotes/websockets-server@workspace:packages/websockets" dependencies: "@newrelic/winston-enricher": "npm:^4.0.0" - "@sentry/node": "npm:^7.19.0" + "@sentry/node": "npm:^7.27.0" "@standardnotes/api": "npm:^1.19.0" "@standardnotes/common": "workspace:^" "@standardnotes/domain-events": "workspace:^" @@ -2613,7 +2572,7 @@ __metadata: resolution: "@standardnotes/workspace-server@workspace:packages/workspace" dependencies: "@newrelic/winston-enricher": "npm:^4.0.0" - "@sentry/node": "npm:^7.19.0" + "@sentry/node": "npm:^7.27.0" "@standardnotes/api": "npm:^1.19.0" "@standardnotes/common": "workspace:*" "@standardnotes/domain-core": "workspace:^"