Browse Source

feat: add api-gateway package

Karol Sójko 3 years ago
parent
commit
57c3b9c29e
53 changed files with 2520 additions and 178 deletions
  1. 125 0
      .github/workflows/api-gateway.release.dev.yml
  2. 1 1
      .github/workflows/files.release.dev.yml
  3. 276 172
      .pnp.cjs
  4. BIN
      .yarn/cache/@standardnotes-auth-npm-3.19.2-5289525e60-2e4b37b303.zip
  5. BIN
      .yarn/cache/@standardnotes-domain-events-infra-npm-1.4.127-18a82f2f72-54e37c296f.zip
  6. BIN
      .yarn/cache/@standardnotes-domain-events-npm-2.29.0-13bec3d9a7-1b68999e2a.zip
  7. BIN
      .yarn/cache/aws-sdk-npm-2.1160.0-1a3db600b7-b95647d4de.zip
  8. BIN
      .yarn/cache/helmet-npm-4.4.1-286ac392ee-cfe385e185.zip
  9. 3 0
      package.json
  10. 33 0
      packages/api-gateway/.env.sample
  11. 2 0
      packages/api-gateway/.eslintignore
  12. 6 0
      packages/api-gateway/.eslintrc
  13. 25 0
      packages/api-gateway/Dockerfile
  14. 75 0
      packages/api-gateway/bin/report.ts
  15. 113 0
      packages/api-gateway/bin/server.ts
  16. 29 0
      packages/api-gateway/docker/entrypoint.sh
  17. 18 0
      packages/api-gateway/jest.config.js
  18. 4 0
      packages/api-gateway/linter.tsconfig.json
  19. 60 0
      packages/api-gateway/package.json
  20. 117 0
      packages/api-gateway/src/Bootstrap/Container.ts
  21. 24 0
      packages/api-gateway/src/Bootstrap/Env.ts
  22. 31 0
      packages/api-gateway/src/Bootstrap/Types.ts
  23. 124 0
      packages/api-gateway/src/Controller/AuthMiddleware.ts
  24. 9 0
      packages/api-gateway/src/Controller/HealthCheckController.ts
  25. 124 0
      packages/api-gateway/src/Controller/LegacyController.ts
  26. 31 0
      packages/api-gateway/src/Controller/StatisticsMiddleware.ts
  27. 120 0
      packages/api-gateway/src/Controller/SubscriptionTokenAuthMiddleware.ts
  28. 4 0
      packages/api-gateway/src/Controller/TokenAuthenticationMethod.ts
  29. 52 0
      packages/api-gateway/src/Controller/v1/ActionsController.ts
  30. 18 0
      packages/api-gateway/src/Controller/v1/FilesController.ts
  31. 17 0
      packages/api-gateway/src/Controller/v1/InvoicesController.ts
  32. 27 0
      packages/api-gateway/src/Controller/v1/ItemsController.ts
  33. 33 0
      packages/api-gateway/src/Controller/v1/OfflineController.ts
  34. 162 0
      packages/api-gateway/src/Controller/v1/PaymentsController.ts
  35. 35 0
      packages/api-gateway/src/Controller/v1/RevisionsController.ts
  36. 34 0
      packages/api-gateway/src/Controller/v1/SessionsController.ts
  37. 42 0
      packages/api-gateway/src/Controller/v1/SubscriptionInvitesController.ts
  38. 18 0
      packages/api-gateway/src/Controller/v1/TokensController.ts
  39. 150 0
      packages/api-gateway/src/Controller/v1/UsersController.ts
  40. 34 0
      packages/api-gateway/src/Controller/v1/WebSocketsController.ts
  41. 23 0
      packages/api-gateway/src/Controller/v2/ActionsControllerV2.ts
  42. 62 0
      packages/api-gateway/src/Controller/v2/PaymentsControllerV2.ts
  43. 46 0
      packages/api-gateway/src/Infra/Redis/RedisCrossServiceTokenCache.ts
  44. 10 0
      packages/api-gateway/src/Service/Cache/CrossServiceTokenCacheInterface.ts
  45. 231 0
      packages/api-gateway/src/Service/Http/HttpService.ts
  46. 34 0
      packages/api-gateway/src/Service/Http/HttpServiceInterface.ts
  47. 0 0
      packages/api-gateway/test-setup.ts
  48. 12 0
      packages/api-gateway/tsconfig.json
  49. 17 0
      packages/api-gateway/wait-for.sh
  50. 1 1
      packages/files/Dockerfile
  51. 1 1
      packages/syncing-server/Dockerfile
  52. 3 0
      tsconfig.json
  53. 104 3
      yarn.lock

+ 125 - 0
.github/workflows/api-gateway.release.dev.yml

@@ -0,0 +1,125 @@
+name: Api Gateway Dev
+
+concurrency:
+  group: api_gateway_dev_environment
+  cancel-in-progress: true
+
+on:
+  push:
+    tags:
+      - '@standardnotes/api-gateway@[0-9]*.[0-9]*.[0-9]*-alpha.[0-9]*'
+      - '@standardnotes/api-gateway@[0-9]*.[0-9]*.[0-9]*-beta.[0-9]*'
+  workflow_dispatch:
+
+jobs:
+  test:
+    runs-on: ubuntu-latest
+
+    steps:
+    - uses: actions/checkout@v3
+    - uses: actions/setup-node@v1
+      with:
+        node-version: '16.x'
+    - run: yarn lint:api-gateway
+
+  publish-aws-ecr:
+    needs: test
+
+    runs-on: ubuntu-latest
+
+    steps:
+    - uses: actions/checkout@v2
+    - name: Configure AWS credentials
+      uses: aws-actions/configure-aws-credentials@v1
+      with:
+        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
+        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
+        aws-region: us-east-1
+    - name: Login to Amazon ECR
+      id: login-ecr
+      uses: aws-actions/amazon-ecr-login@v1
+    - name: Build, tag, and push image to Amazon ECR
+      id: build-image
+      env:
+        ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
+        ECR_REPOSITORY: api-gateway
+        IMAGE_TAG: ${{ github.sha }}
+      run: |
+        yarn docker build @standardnotes/api-gateway -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
+        docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
+        docker tag $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG $ECR_REGISTRY/$ECR_REPOSITORY:dev
+        docker push $ECR_REGISTRY/$ECR_REPOSITORY:dev
+
+  publish-docker-hub:
+    needs: test
+
+    runs-on: ubuntu-latest
+
+    steps:
+    - uses: actions/checkout@v2
+    - name: Publish to Registry
+      uses: elgohr/Publish-Docker-Github-Action@master
+      with:
+        name: standardnotes/api-gateway
+        username: ${{ secrets.DOCKER_USERNAME }}
+        password: ${{ secrets.DOCKER_PASSWORD }}
+        tags: "dev,${{ github.sha }}"
+
+  deploy-web:
+    needs: publish-aws-ecr
+
+    runs-on: ubuntu-latest
+
+    steps:
+    - name: Configure AWS credentials
+      uses: aws-actions/configure-aws-credentials@v1
+      with:
+        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
+        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
+        aws-region: us-east-1
+    - name: Download task definition
+      run: |
+        aws ecs describe-task-definition --task-definition api-gateway-dev --query taskDefinition > task-definition.json
+    - name: Fill in the new version in the Amazon ECS task definition
+      run: |
+        jq '(.containerDefinitions[] | select(.name=="api-gateway-dev") | .environment[] | select(.name=="VERSION")).value = "${{ github.sha }}"' task-definition.json > tmp.json && mv tmp.json task-definition.json
+    - name: Fill in the new image ID in the Amazon ECS task definition
+      id: task-def
+      uses: aws-actions/amazon-ecs-render-task-definition@v1
+      with:
+        task-definition: task-definition.json
+        container-name: api-gateway-dev
+        image: ${{ secrets.AWS_ECR_REGISTRY }}/api-gateway:${{ github.sha }}
+    - name: Deploy Amazon ECS task definition
+      uses: aws-actions/amazon-ecs-deploy-task-definition@v1
+      with:
+        task-definition: ${{ steps.task-def.outputs.task-definition }}
+        service: api-gateway-dev
+        cluster: dev
+        wait-for-service-stability: true
+
+  newrelic:
+    needs: deploy-web
+
+    runs-on: ubuntu-latest
+    steps:
+      - name: Create New Relic deployment marker for Web
+        uses: newrelic/deployment-marker-action@v1
+        with:
+          accountId: ${{ secrets.NEW_RELIC_ACCOUNT_ID }}
+          apiKey: ${{ secrets.NEW_RELIC_API_KEY }}
+          applicationId: ${{ secrets.NEW_RELIC_APPLICATION_ID_API_GATEWAY_WEB_DEV }}
+          revision: "${{ github.sha }}"
+          description: "Automated Deployment via Github Actions"
+          user: "${{ github.actor }}"
+
+  notify_discord:
+    needs: deploy-web
+
+    runs-on: ubuntu-latest
+
+    steps:
+    - name: Run Discord Webhook
+      uses: johnnyhuy/actions-discord-git-webhook@main
+      with:
+        webhook_url: ${{ secrets.DISCORD_WEBHOOK_URL }}

+ 1 - 1
.github/workflows/files.release.dev.yml

@@ -46,7 +46,7 @@ jobs:
         ECR_REPOSITORY: files
         IMAGE_TAG: ${{ github.sha }}
       run: |
-        yarn docker build @standardnotes/files -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
+        yarn docker build @standardnotes/files-server -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
         docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
         docker tag $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG $ECR_REGISTRY/$ECR_REPOSITORY:dev
         docker push $ECR_REGISTRY/$ECR_REPOSITORY:dev

+ 276 - 172
.pnp.cjs

@@ -20,6 +20,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
         "name": "@standardnotes/server-monorepo",\
         "reference": "workspace:."\
       },\
+      {\
+        "name": "@standardnotes/api-gateway",\
+        "reference": "workspace:packages/api-gateway"\
+      },\
       {\
         "name": "@standardnotes/auth-server",\
         "reference": "workspace:packages/auth"\
@@ -40,6 +44,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
     "enableTopLevelFallback": true,\
     "ignorePatternData": "(^(?:\\\\.yarn\\\\/sdks(?:\\\\/(?!\\\\.{1,2}(?:\\\\/|$))(?:(?:(?!(?:^|\\\\/)\\\\.{1,2}(?:\\\\/|$)).)*?)|$))$)",\
     "fallbackExclusionList": [\
+      ["@standardnotes/api-gateway", ["workspace:packages/api-gateway"]],\
       ["@standardnotes/auth-server", ["workspace:packages/auth"]],\
       ["@standardnotes/files-server", ["workspace:packages/files"]],\
       ["@standardnotes/scheduler-server", ["workspace:packages/scheduler"]],\
@@ -307,10 +312,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
           ],\
           "linkType": "SOFT"\
         }],\
-        ["virtual:511f18ec4797ab42da93993c808942012470709446ef400bc600a66a60e5e382e8081da234db4fe66d41d38d9212735169fb3c604c828d1ff8dc854506ed7b48#npm:7.8.4", {\
-          "packageLocation": "./.yarn/__virtual__/@babel-plugin-syntax-async-generators-virtual-1c91e87c25/0/cache/@babel-plugin-syntax-async-generators-npm-7.8.4-d10cf993c9-7ed1c1d9b9.zip/node_modules/@babel/plugin-syntax-async-generators/",\
+        ["virtual:c373874e1baa9d76ffb6ce2109646b13db6b2b433728eac72e69f1da2ca3718a077c7549e49925b0fa06952690278e6a36ceca4ebe492de934ac9dea98343dd7#npm:7.8.4", {\
+          "packageLocation": "./.yarn/__virtual__/@babel-plugin-syntax-async-generators-virtual-20dc503383/0/cache/@babel-plugin-syntax-async-generators-npm-7.8.4-d10cf993c9-7ed1c1d9b9.zip/node_modules/@babel/plugin-syntax-async-generators/",\
           "packageDependencies": [\
-            ["@babel/plugin-syntax-async-generators", "virtual:511f18ec4797ab42da93993c808942012470709446ef400bc600a66a60e5e382e8081da234db4fe66d41d38d9212735169fb3c604c828d1ff8dc854506ed7b48#npm:7.8.4"],\
+            ["@babel/plugin-syntax-async-generators", "virtual:c373874e1baa9d76ffb6ce2109646b13db6b2b433728eac72e69f1da2ca3718a077c7549e49925b0fa06952690278e6a36ceca4ebe492de934ac9dea98343dd7#npm:7.8.4"],\
             ["@babel/core", "npm:7.18.5"],\
             ["@babel/helper-plugin-utils", "npm:7.17.12"],\
             ["@types/babel__core", "npm:7.1.19"]\
@@ -344,10 +349,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
           ],\
           "linkType": "SOFT"\
         }],\
-        ["virtual:511f18ec4797ab42da93993c808942012470709446ef400bc600a66a60e5e382e8081da234db4fe66d41d38d9212735169fb3c604c828d1ff8dc854506ed7b48#npm:7.8.3", {\
-          "packageLocation": "./.yarn/__virtual__/@babel-plugin-syntax-bigint-virtual-62067a9335/0/cache/@babel-plugin-syntax-bigint-npm-7.8.3-b05d971e6c-3a10849d83.zip/node_modules/@babel/plugin-syntax-bigint/",\
+        ["virtual:c373874e1baa9d76ffb6ce2109646b13db6b2b433728eac72e69f1da2ca3718a077c7549e49925b0fa06952690278e6a36ceca4ebe492de934ac9dea98343dd7#npm:7.8.3", {\
+          "packageLocation": "./.yarn/__virtual__/@babel-plugin-syntax-bigint-virtual-f9f6a1f31a/0/cache/@babel-plugin-syntax-bigint-npm-7.8.3-b05d971e6c-3a10849d83.zip/node_modules/@babel/plugin-syntax-bigint/",\
           "packageDependencies": [\
-            ["@babel/plugin-syntax-bigint", "virtual:511f18ec4797ab42da93993c808942012470709446ef400bc600a66a60e5e382e8081da234db4fe66d41d38d9212735169fb3c604c828d1ff8dc854506ed7b48#npm:7.8.3"],\
+            ["@babel/plugin-syntax-bigint", "virtual:c373874e1baa9d76ffb6ce2109646b13db6b2b433728eac72e69f1da2ca3718a077c7549e49925b0fa06952690278e6a36ceca4ebe492de934ac9dea98343dd7#npm:7.8.3"],\
             ["@babel/core", "npm:7.18.5"],\
             ["@babel/helper-plugin-utils", "npm:7.17.12"],\
             ["@types/babel__core", "npm:7.1.19"]\
@@ -381,10 +386,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
           ],\
           "linkType": "SOFT"\
         }],\
-        ["virtual:511f18ec4797ab42da93993c808942012470709446ef400bc600a66a60e5e382e8081da234db4fe66d41d38d9212735169fb3c604c828d1ff8dc854506ed7b48#npm:7.12.13", {\
-          "packageLocation": "./.yarn/__virtual__/@babel-plugin-syntax-class-properties-virtual-c73477fb62/0/cache/@babel-plugin-syntax-class-properties-npm-7.12.13-002ee9d930-24f34b196d.zip/node_modules/@babel/plugin-syntax-class-properties/",\
+        ["virtual:c373874e1baa9d76ffb6ce2109646b13db6b2b433728eac72e69f1da2ca3718a077c7549e49925b0fa06952690278e6a36ceca4ebe492de934ac9dea98343dd7#npm:7.12.13", {\
+          "packageLocation": "./.yarn/__virtual__/@babel-plugin-syntax-class-properties-virtual-d36699afc2/0/cache/@babel-plugin-syntax-class-properties-npm-7.12.13-002ee9d930-24f34b196d.zip/node_modules/@babel/plugin-syntax-class-properties/",\
           "packageDependencies": [\
-            ["@babel/plugin-syntax-class-properties", "virtual:511f18ec4797ab42da93993c808942012470709446ef400bc600a66a60e5e382e8081da234db4fe66d41d38d9212735169fb3c604c828d1ff8dc854506ed7b48#npm:7.12.13"],\
+            ["@babel/plugin-syntax-class-properties", "virtual:c373874e1baa9d76ffb6ce2109646b13db6b2b433728eac72e69f1da2ca3718a077c7549e49925b0fa06952690278e6a36ceca4ebe492de934ac9dea98343dd7#npm:7.12.13"],\
             ["@babel/core", "npm:7.18.5"],\
             ["@babel/helper-plugin-utils", "npm:7.17.12"],\
             ["@types/babel__core", "npm:7.1.19"]\
@@ -418,10 +423,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
           ],\
           "linkType": "SOFT"\
         }],\
-        ["virtual:511f18ec4797ab42da93993c808942012470709446ef400bc600a66a60e5e382e8081da234db4fe66d41d38d9212735169fb3c604c828d1ff8dc854506ed7b48#npm:7.10.4", {\
-          "packageLocation": "./.yarn/__virtual__/@babel-plugin-syntax-import-meta-virtual-f94e1adec9/0/cache/@babel-plugin-syntax-import-meta-npm-7.10.4-4a0a0158bc-166ac1125d.zip/node_modules/@babel/plugin-syntax-import-meta/",\
+        ["virtual:c373874e1baa9d76ffb6ce2109646b13db6b2b433728eac72e69f1da2ca3718a077c7549e49925b0fa06952690278e6a36ceca4ebe492de934ac9dea98343dd7#npm:7.10.4", {\
+          "packageLocation": "./.yarn/__virtual__/@babel-plugin-syntax-import-meta-virtual-f872deb0c7/0/cache/@babel-plugin-syntax-import-meta-npm-7.10.4-4a0a0158bc-166ac1125d.zip/node_modules/@babel/plugin-syntax-import-meta/",\
           "packageDependencies": [\
-            ["@babel/plugin-syntax-import-meta", "virtual:511f18ec4797ab42da93993c808942012470709446ef400bc600a66a60e5e382e8081da234db4fe66d41d38d9212735169fb3c604c828d1ff8dc854506ed7b48#npm:7.10.4"],\
+            ["@babel/plugin-syntax-import-meta", "virtual:c373874e1baa9d76ffb6ce2109646b13db6b2b433728eac72e69f1da2ca3718a077c7549e49925b0fa06952690278e6a36ceca4ebe492de934ac9dea98343dd7#npm:7.10.4"],\
             ["@babel/core", "npm:7.18.5"],\
             ["@babel/helper-plugin-utils", "npm:7.17.12"],\
             ["@types/babel__core", "npm:7.1.19"]\
@@ -455,10 +460,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
           ],\
           "linkType": "SOFT"\
         }],\
-        ["virtual:511f18ec4797ab42da93993c808942012470709446ef400bc600a66a60e5e382e8081da234db4fe66d41d38d9212735169fb3c604c828d1ff8dc854506ed7b48#npm:7.8.3", {\
-          "packageLocation": "./.yarn/__virtual__/@babel-plugin-syntax-json-strings-virtual-b788fe66a9/0/cache/@babel-plugin-syntax-json-strings-npm-7.8.3-6dc7848179-bf5aea1f31.zip/node_modules/@babel/plugin-syntax-json-strings/",\
+        ["virtual:c373874e1baa9d76ffb6ce2109646b13db6b2b433728eac72e69f1da2ca3718a077c7549e49925b0fa06952690278e6a36ceca4ebe492de934ac9dea98343dd7#npm:7.8.3", {\
+          "packageLocation": "./.yarn/__virtual__/@babel-plugin-syntax-json-strings-virtual-b38277fec2/0/cache/@babel-plugin-syntax-json-strings-npm-7.8.3-6dc7848179-bf5aea1f31.zip/node_modules/@babel/plugin-syntax-json-strings/",\
           "packageDependencies": [\
-            ["@babel/plugin-syntax-json-strings", "virtual:511f18ec4797ab42da93993c808942012470709446ef400bc600a66a60e5e382e8081da234db4fe66d41d38d9212735169fb3c604c828d1ff8dc854506ed7b48#npm:7.8.3"],\
+            ["@babel/plugin-syntax-json-strings", "virtual:c373874e1baa9d76ffb6ce2109646b13db6b2b433728eac72e69f1da2ca3718a077c7549e49925b0fa06952690278e6a36ceca4ebe492de934ac9dea98343dd7#npm:7.8.3"],\
             ["@babel/core", "npm:7.18.5"],\
             ["@babel/helper-plugin-utils", "npm:7.17.12"],\
             ["@types/babel__core", "npm:7.1.19"]\
@@ -492,10 +497,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
           ],\
           "linkType": "SOFT"\
         }],\
-        ["virtual:511f18ec4797ab42da93993c808942012470709446ef400bc600a66a60e5e382e8081da234db4fe66d41d38d9212735169fb3c604c828d1ff8dc854506ed7b48#npm:7.10.4", {\
-          "packageLocation": "./.yarn/__virtual__/@babel-plugin-syntax-logical-assignment-operators-virtual-00106656c0/0/cache/@babel-plugin-syntax-logical-assignment-operators-npm-7.10.4-72ae00fdf6-aff3357703.zip/node_modules/@babel/plugin-syntax-logical-assignment-operators/",\
+        ["virtual:c373874e1baa9d76ffb6ce2109646b13db6b2b433728eac72e69f1da2ca3718a077c7549e49925b0fa06952690278e6a36ceca4ebe492de934ac9dea98343dd7#npm:7.10.4", {\
+          "packageLocation": "./.yarn/__virtual__/@babel-plugin-syntax-logical-assignment-operators-virtual-f4f5745024/0/cache/@babel-plugin-syntax-logical-assignment-operators-npm-7.10.4-72ae00fdf6-aff3357703.zip/node_modules/@babel/plugin-syntax-logical-assignment-operators/",\
           "packageDependencies": [\
-            ["@babel/plugin-syntax-logical-assignment-operators", "virtual:511f18ec4797ab42da93993c808942012470709446ef400bc600a66a60e5e382e8081da234db4fe66d41d38d9212735169fb3c604c828d1ff8dc854506ed7b48#npm:7.10.4"],\
+            ["@babel/plugin-syntax-logical-assignment-operators", "virtual:c373874e1baa9d76ffb6ce2109646b13db6b2b433728eac72e69f1da2ca3718a077c7549e49925b0fa06952690278e6a36ceca4ebe492de934ac9dea98343dd7#npm:7.10.4"],\
             ["@babel/core", "npm:7.18.5"],\
             ["@babel/helper-plugin-utils", "npm:7.17.12"],\
             ["@types/babel__core", "npm:7.1.19"]\
@@ -529,10 +534,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
           ],\
           "linkType": "SOFT"\
         }],\
-        ["virtual:511f18ec4797ab42da93993c808942012470709446ef400bc600a66a60e5e382e8081da234db4fe66d41d38d9212735169fb3c604c828d1ff8dc854506ed7b48#npm:7.8.3", {\
-          "packageLocation": "./.yarn/__virtual__/@babel-plugin-syntax-nullish-coalescing-operator-virtual-0e94aeb633/0/cache/@babel-plugin-syntax-nullish-coalescing-operator-npm-7.8.3-8a723173b5-87aca49189.zip/node_modules/@babel/plugin-syntax-nullish-coalescing-operator/",\
+        ["virtual:c373874e1baa9d76ffb6ce2109646b13db6b2b433728eac72e69f1da2ca3718a077c7549e49925b0fa06952690278e6a36ceca4ebe492de934ac9dea98343dd7#npm:7.8.3", {\
+          "packageLocation": "./.yarn/__virtual__/@babel-plugin-syntax-nullish-coalescing-operator-virtual-7cd00d383d/0/cache/@babel-plugin-syntax-nullish-coalescing-operator-npm-7.8.3-8a723173b5-87aca49189.zip/node_modules/@babel/plugin-syntax-nullish-coalescing-operator/",\
           "packageDependencies": [\
-            ["@babel/plugin-syntax-nullish-coalescing-operator", "virtual:511f18ec4797ab42da93993c808942012470709446ef400bc600a66a60e5e382e8081da234db4fe66d41d38d9212735169fb3c604c828d1ff8dc854506ed7b48#npm:7.8.3"],\
+            ["@babel/plugin-syntax-nullish-coalescing-operator", "virtual:c373874e1baa9d76ffb6ce2109646b13db6b2b433728eac72e69f1da2ca3718a077c7549e49925b0fa06952690278e6a36ceca4ebe492de934ac9dea98343dd7#npm:7.8.3"],\
             ["@babel/core", "npm:7.18.5"],\
             ["@babel/helper-plugin-utils", "npm:7.17.12"],\
             ["@types/babel__core", "npm:7.1.19"]\
@@ -566,10 +571,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
           ],\
           "linkType": "SOFT"\
         }],\
-        ["virtual:511f18ec4797ab42da93993c808942012470709446ef400bc600a66a60e5e382e8081da234db4fe66d41d38d9212735169fb3c604c828d1ff8dc854506ed7b48#npm:7.10.4", {\
-          "packageLocation": "./.yarn/__virtual__/@babel-plugin-syntax-numeric-separator-virtual-ae7862c24d/0/cache/@babel-plugin-syntax-numeric-separator-npm-7.10.4-81444be605-01ec5547bd.zip/node_modules/@babel/plugin-syntax-numeric-separator/",\
+        ["virtual:c373874e1baa9d76ffb6ce2109646b13db6b2b433728eac72e69f1da2ca3718a077c7549e49925b0fa06952690278e6a36ceca4ebe492de934ac9dea98343dd7#npm:7.10.4", {\
+          "packageLocation": "./.yarn/__virtual__/@babel-plugin-syntax-numeric-separator-virtual-1f39443676/0/cache/@babel-plugin-syntax-numeric-separator-npm-7.10.4-81444be605-01ec5547bd.zip/node_modules/@babel/plugin-syntax-numeric-separator/",\
           "packageDependencies": [\
-            ["@babel/plugin-syntax-numeric-separator", "virtual:511f18ec4797ab42da93993c808942012470709446ef400bc600a66a60e5e382e8081da234db4fe66d41d38d9212735169fb3c604c828d1ff8dc854506ed7b48#npm:7.10.4"],\
+            ["@babel/plugin-syntax-numeric-separator", "virtual:c373874e1baa9d76ffb6ce2109646b13db6b2b433728eac72e69f1da2ca3718a077c7549e49925b0fa06952690278e6a36ceca4ebe492de934ac9dea98343dd7#npm:7.10.4"],\
             ["@babel/core", "npm:7.18.5"],\
             ["@babel/helper-plugin-utils", "npm:7.17.12"],\
             ["@types/babel__core", "npm:7.1.19"]\
@@ -603,10 +608,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
           ],\
           "linkType": "SOFT"\
         }],\
-        ["virtual:511f18ec4797ab42da93993c808942012470709446ef400bc600a66a60e5e382e8081da234db4fe66d41d38d9212735169fb3c604c828d1ff8dc854506ed7b48#npm:7.8.3", {\
-          "packageLocation": "./.yarn/__virtual__/@babel-plugin-syntax-object-rest-spread-virtual-70d6d17cdf/0/cache/@babel-plugin-syntax-object-rest-spread-npm-7.8.3-60bd05b6ae-fddcf581a5.zip/node_modules/@babel/plugin-syntax-object-rest-spread/",\
+        ["virtual:c373874e1baa9d76ffb6ce2109646b13db6b2b433728eac72e69f1da2ca3718a077c7549e49925b0fa06952690278e6a36ceca4ebe492de934ac9dea98343dd7#npm:7.8.3", {\
+          "packageLocation": "./.yarn/__virtual__/@babel-plugin-syntax-object-rest-spread-virtual-e635fc53df/0/cache/@babel-plugin-syntax-object-rest-spread-npm-7.8.3-60bd05b6ae-fddcf581a5.zip/node_modules/@babel/plugin-syntax-object-rest-spread/",\
           "packageDependencies": [\
-            ["@babel/plugin-syntax-object-rest-spread", "virtual:511f18ec4797ab42da93993c808942012470709446ef400bc600a66a60e5e382e8081da234db4fe66d41d38d9212735169fb3c604c828d1ff8dc854506ed7b48#npm:7.8.3"],\
+            ["@babel/plugin-syntax-object-rest-spread", "virtual:c373874e1baa9d76ffb6ce2109646b13db6b2b433728eac72e69f1da2ca3718a077c7549e49925b0fa06952690278e6a36ceca4ebe492de934ac9dea98343dd7#npm:7.8.3"],\
             ["@babel/core", "npm:7.18.5"],\
             ["@babel/helper-plugin-utils", "npm:7.17.12"],\
             ["@types/babel__core", "npm:7.1.19"]\
@@ -640,10 +645,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
           ],\
           "linkType": "SOFT"\
         }],\
-        ["virtual:511f18ec4797ab42da93993c808942012470709446ef400bc600a66a60e5e382e8081da234db4fe66d41d38d9212735169fb3c604c828d1ff8dc854506ed7b48#npm:7.8.3", {\
-          "packageLocation": "./.yarn/__virtual__/@babel-plugin-syntax-optional-catch-binding-virtual-19cae39d4e/0/cache/@babel-plugin-syntax-optional-catch-binding-npm-7.8.3-ce337427d8-910d90e72b.zip/node_modules/@babel/plugin-syntax-optional-catch-binding/",\
+        ["virtual:c373874e1baa9d76ffb6ce2109646b13db6b2b433728eac72e69f1da2ca3718a077c7549e49925b0fa06952690278e6a36ceca4ebe492de934ac9dea98343dd7#npm:7.8.3", {\
+          "packageLocation": "./.yarn/__virtual__/@babel-plugin-syntax-optional-catch-binding-virtual-7faf06b837/0/cache/@babel-plugin-syntax-optional-catch-binding-npm-7.8.3-ce337427d8-910d90e72b.zip/node_modules/@babel/plugin-syntax-optional-catch-binding/",\
           "packageDependencies": [\
-            ["@babel/plugin-syntax-optional-catch-binding", "virtual:511f18ec4797ab42da93993c808942012470709446ef400bc600a66a60e5e382e8081da234db4fe66d41d38d9212735169fb3c604c828d1ff8dc854506ed7b48#npm:7.8.3"],\
+            ["@babel/plugin-syntax-optional-catch-binding", "virtual:c373874e1baa9d76ffb6ce2109646b13db6b2b433728eac72e69f1da2ca3718a077c7549e49925b0fa06952690278e6a36ceca4ebe492de934ac9dea98343dd7#npm:7.8.3"],\
             ["@babel/core", "npm:7.18.5"],\
             ["@babel/helper-plugin-utils", "npm:7.17.12"],\
             ["@types/babel__core", "npm:7.1.19"]\
@@ -677,10 +682,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
           ],\
           "linkType": "SOFT"\
         }],\
-        ["virtual:511f18ec4797ab42da93993c808942012470709446ef400bc600a66a60e5e382e8081da234db4fe66d41d38d9212735169fb3c604c828d1ff8dc854506ed7b48#npm:7.8.3", {\
-          "packageLocation": "./.yarn/__virtual__/@babel-plugin-syntax-optional-chaining-virtual-0faf2c8e15/0/cache/@babel-plugin-syntax-optional-chaining-npm-7.8.3-f3f3c79579-eef94d53a1.zip/node_modules/@babel/plugin-syntax-optional-chaining/",\
+        ["virtual:c373874e1baa9d76ffb6ce2109646b13db6b2b433728eac72e69f1da2ca3718a077c7549e49925b0fa06952690278e6a36ceca4ebe492de934ac9dea98343dd7#npm:7.8.3", {\
+          "packageLocation": "./.yarn/__virtual__/@babel-plugin-syntax-optional-chaining-virtual-cd0a3a9619/0/cache/@babel-plugin-syntax-optional-chaining-npm-7.8.3-f3f3c79579-eef94d53a1.zip/node_modules/@babel/plugin-syntax-optional-chaining/",\
           "packageDependencies": [\
-            ["@babel/plugin-syntax-optional-chaining", "virtual:511f18ec4797ab42da93993c808942012470709446ef400bc600a66a60e5e382e8081da234db4fe66d41d38d9212735169fb3c604c828d1ff8dc854506ed7b48#npm:7.8.3"],\
+            ["@babel/plugin-syntax-optional-chaining", "virtual:c373874e1baa9d76ffb6ce2109646b13db6b2b433728eac72e69f1da2ca3718a077c7549e49925b0fa06952690278e6a36ceca4ebe492de934ac9dea98343dd7#npm:7.8.3"],\
             ["@babel/core", "npm:7.18.5"],\
             ["@babel/helper-plugin-utils", "npm:7.17.12"],\
             ["@types/babel__core", "npm:7.1.19"]\
@@ -714,10 +719,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
           ],\
           "linkType": "SOFT"\
         }],\
-        ["virtual:511f18ec4797ab42da93993c808942012470709446ef400bc600a66a60e5e382e8081da234db4fe66d41d38d9212735169fb3c604c828d1ff8dc854506ed7b48#npm:7.14.5", {\
-          "packageLocation": "./.yarn/__virtual__/@babel-plugin-syntax-top-level-await-virtual-4f23029d05/0/cache/@babel-plugin-syntax-top-level-await-npm-7.14.5-60a0a2e83b-bbd1a56b09.zip/node_modules/@babel/plugin-syntax-top-level-await/",\
+        ["virtual:c373874e1baa9d76ffb6ce2109646b13db6b2b433728eac72e69f1da2ca3718a077c7549e49925b0fa06952690278e6a36ceca4ebe492de934ac9dea98343dd7#npm:7.14.5", {\
+          "packageLocation": "./.yarn/__virtual__/@babel-plugin-syntax-top-level-await-virtual-ea96b657a7/0/cache/@babel-plugin-syntax-top-level-await-npm-7.14.5-60a0a2e83b-bbd1a56b09.zip/node_modules/@babel/plugin-syntax-top-level-await/",\
           "packageDependencies": [\
-            ["@babel/plugin-syntax-top-level-await", "virtual:511f18ec4797ab42da93993c808942012470709446ef400bc600a66a60e5e382e8081da234db4fe66d41d38d9212735169fb3c604c828d1ff8dc854506ed7b48#npm:7.14.5"],\
+            ["@babel/plugin-syntax-top-level-await", "virtual:c373874e1baa9d76ffb6ce2109646b13db6b2b433728eac72e69f1da2ca3718a077c7549e49925b0fa06952690278e6a36ceca4ebe492de934ac9dea98343dd7#npm:7.14.5"],\
             ["@babel/core", "npm:7.18.5"],\
             ["@babel/helper-plugin-utils", "npm:7.17.12"],\
             ["@types/babel__core", "npm:7.1.19"]\
@@ -1201,12 +1206,12 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
           ],\
           "linkType": "SOFT"\
         }],\
-        ["virtual:6e20fd2eaaa940ccda315da6252a82baa2918f5ea3c40e2d7cb4d97f01b503d35a5076b4b63a33762fb1174e73a3313072cadf65e4a26d1b33660f964eda7880#npm:28.1.1", {\
-          "packageLocation": "./.yarn/__virtual__/@jest-core-virtual-4b45c3242e/0/cache/@jest-core-npm-28.1.1-fb910fbf90-fd4361f77b.zip/node_modules/@jest/core/",\
+        ["virtual:c6df9164d8eb22b719e1cd2b32bc24b485c129a3b28db796c6f3a6c22ad9410bc922a7f59cbbfe78d6492c9ee9f75de8ed7c2ec2f694d58e500097e2a2b74028#npm:28.1.1", {\
+          "packageLocation": "./.yarn/__virtual__/@jest-core-virtual-c650136efc/0/cache/@jest-core-npm-28.1.1-fb910fbf90-fd4361f77b.zip/node_modules/@jest/core/",\
           "packageDependencies": [\
-            ["@jest/core", "virtual:6e20fd2eaaa940ccda315da6252a82baa2918f5ea3c40e2d7cb4d97f01b503d35a5076b4b63a33762fb1174e73a3313072cadf65e4a26d1b33660f964eda7880#npm:28.1.1"],\
+            ["@jest/core", "virtual:c6df9164d8eb22b719e1cd2b32bc24b485c129a3b28db796c6f3a6c22ad9410bc922a7f59cbbfe78d6492c9ee9f75de8ed7c2ec2f694d58e500097e2a2b74028#npm:28.1.1"],\
             ["@jest/console", "npm:28.1.1"],\
-            ["@jest/reporters", "virtual:4b45c3242ed36b84511b3946081e5d3b347e0463d6e39ebfdee2ad8392eb4bd7a5761a69e4fccf0d230c488b171720ddcf381e7c249fe8f4fcdf9d4afc493b87#npm:28.1.1"],\
+            ["@jest/reporters", "virtual:c650136efccbbb479cf2d5b8062777752af30a07867b5d5c0bde3cbc6a5c6f054afdfcc433e9ded24592d8fb0664ba4983e4f355a06678d9210b9c9e34f577b9#npm:28.1.1"],\
             ["@jest/test-result", "npm:28.1.1"],\
             ["@jest/transform", "npm:28.1.1"],\
             ["@jest/types", "npm:28.1.1"],\
@@ -1218,7 +1223,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
             ["exit", "npm:0.1.2"],\
             ["graceful-fs", "npm:4.2.10"],\
             ["jest-changed-files", "npm:28.0.2"],\
-            ["jest-config", "virtual:4b45c3242ed36b84511b3946081e5d3b347e0463d6e39ebfdee2ad8392eb4bd7a5761a69e4fccf0d230c488b171720ddcf381e7c249fe8f4fcdf9d4afc493b87#npm:28.1.1"],\
+            ["jest-config", "virtual:c650136efccbbb479cf2d5b8062777752af30a07867b5d5c0bde3cbc6a5c6f054afdfcc433e9ded24592d8fb0664ba4983e4f355a06678d9210b9c9e34f577b9#npm:28.1.1"],\
             ["jest-haste-map", "npm:28.1.1"],\
             ["jest-message-util", "npm:28.1.1"],\
             ["jest-regex-util", "npm:28.0.2"],\
@@ -1313,10 +1318,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
           ],\
           "linkType": "SOFT"\
         }],\
-        ["virtual:4b45c3242ed36b84511b3946081e5d3b347e0463d6e39ebfdee2ad8392eb4bd7a5761a69e4fccf0d230c488b171720ddcf381e7c249fe8f4fcdf9d4afc493b87#npm:28.1.1", {\
-          "packageLocation": "./.yarn/__virtual__/@jest-reporters-virtual-ea52091ed4/0/cache/@jest-reporters-npm-28.1.1-21fe131d02-8ad68d4a93.zip/node_modules/@jest/reporters/",\
+        ["virtual:c650136efccbbb479cf2d5b8062777752af30a07867b5d5c0bde3cbc6a5c6f054afdfcc433e9ded24592d8fb0664ba4983e4f355a06678d9210b9c9e34f577b9#npm:28.1.1", {\
+          "packageLocation": "./.yarn/__virtual__/@jest-reporters-virtual-4b439ab9a1/0/cache/@jest-reporters-npm-28.1.1-21fe131d02-8ad68d4a93.zip/node_modules/@jest/reporters/",\
           "packageDependencies": [\
-            ["@jest/reporters", "virtual:4b45c3242ed36b84511b3946081e5d3b347e0463d6e39ebfdee2ad8392eb4bd7a5761a69e4fccf0d230c488b171720ddcf381e7c249fe8f4fcdf9d4afc493b87#npm:28.1.1"],\
+            ["@jest/reporters", "virtual:c650136efccbbb479cf2d5b8062777752af30a07867b5d5c0bde3cbc6a5c6f054afdfcc433e9ded24592d8fb0664ba4983e4f355a06678d9210b9c9e34f577b9#npm:28.1.1"],\
             ["@bcoe/v8-coverage", "npm:0.2.3"],\
             ["@jest/console", "npm:28.1.1"],\
             ["@jest/test-result", "npm:28.1.1"],\
@@ -1944,10 +1949,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
           ],\
           "linkType": "SOFT"\
         }],\
-        ["virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:2.1.2", {\
-          "packageLocation": "./.yarn/__virtual__/@newrelic-winston-enricher-virtual-30a09b12b4/0/cache/@newrelic-winston-enricher-npm-2.1.2-732878a1b2-d001c13166.zip/node_modules/@newrelic/winston-enricher/",\
+        ["virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:2.1.2", {\
+          "packageLocation": "./.yarn/__virtual__/@newrelic-winston-enricher-virtual-193127fbcd/0/cache/@newrelic-winston-enricher-npm-2.1.2-732878a1b2-d001c13166.zip/node_modules/@newrelic/winston-enricher/",\
           "packageDependencies": [\
-            ["@newrelic/winston-enricher", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:2.1.2"],\
+            ["@newrelic/winston-enricher", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:2.1.2"],\
             ["@types/newrelic", "npm:7.0.3"],\
             ["@types/winston", null],\
             ["newrelic", "npm:8.6.0"],\
@@ -2584,7 +2589,60 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
           "linkType": "HARD"\
         }]\
       ]],\
+      ["@standardnotes/api-gateway", [\
+        ["workspace:packages/api-gateway", {\
+          "packageLocation": "./packages/api-gateway/",\
+          "packageDependencies": [\
+            ["@standardnotes/api-gateway", "workspace:packages/api-gateway"],\
+            ["@newrelic/native-metrics", "npm:7.0.2"],\
+            ["@newrelic/winston-enricher", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:2.1.2"],\
+            ["@sentry/node", "npm:6.19.7"],\
+            ["@standardnotes/analytics", "npm:1.6.0"],\
+            ["@standardnotes/auth", "npm:3.19.2"],\
+            ["@standardnotes/domain-events", "npm:2.29.0"],\
+            ["@standardnotes/domain-events-infra", "npm:1.4.127"],\
+            ["@standardnotes/time", "npm:1.7.0"],\
+            ["@types/cors", "npm:2.8.12"],\
+            ["@types/express", "npm:4.17.13"],\
+            ["@types/ioredis", "npm:4.28.10"],\
+            ["@types/jest", "npm:28.1.3"],\
+            ["@types/jsonwebtoken", "npm:8.5.8"],\
+            ["@types/newrelic", "npm:7.0.3"],\
+            ["@types/prettyjson", "npm:0.0.29"],\
+            ["@typescript-eslint/eslint-plugin", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:5.29.0"],\
+            ["aws-sdk", "npm:2.1160.0"],\
+            ["axios", "npm:0.24.0"],\
+            ["cors", "npm:2.8.5"],\
+            ["dotenv", "npm:8.2.0"],\
+            ["eslint", "npm:8.18.0"],\
+            ["eslint-plugin-prettier", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:4.0.0"],\
+            ["express", "npm:4.17.1"],\
+            ["helmet", "npm:4.4.1"],\
+            ["inversify", "npm:6.0.1"],\
+            ["inversify-express-utils", "npm:6.4.3"],\
+            ["ioredis", "npm:5.0.6"],\
+            ["jest", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:28.1.1"],\
+            ["jsonwebtoken", "npm:8.5.1"],\
+            ["newrelic", "npm:8.6.0"],\
+            ["nodemon", "npm:2.0.16"],\
+            ["prettyjson", "npm:1.2.1"],\
+            ["reflect-metadata", "npm:0.1.13"],\
+            ["ts-jest", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:28.0.5"],\
+            ["winston", "npm:3.3.3"]\
+          ],\
+          "linkType": "SOFT"\
+        }]\
+      ]],\
       ["@standardnotes/auth", [\
+        ["npm:3.19.2", {\
+          "packageLocation": "./.yarn/cache/@standardnotes-auth-npm-3.19.2-5289525e60-2e4b37b303.zip/node_modules/@standardnotes/auth/",\
+          "packageDependencies": [\
+            ["@standardnotes/auth", "npm:3.19.2"],\
+            ["@standardnotes/common", "npm:1.23.0"],\
+            ["jsonwebtoken", "npm:8.5.1"]\
+          ],\
+          "linkType": "HARD"\
+        }],\
         ["npm:3.19.3", {\
           "packageLocation": "./.yarn/cache/@standardnotes-auth-npm-3.19.3-c77ec60e52-7e421b5eaf.zip/node_modules/@standardnotes/auth/",\
           "packageDependencies": [\
@@ -2601,7 +2659,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
           "packageDependencies": [\
             ["@standardnotes/auth-server", "workspace:packages/auth"],\
             ["@newrelic/native-metrics", "npm:7.0.2"],\
-            ["@newrelic/winston-enricher", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:2.1.2"],\
+            ["@newrelic/winston-enricher", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:2.1.2"],\
             ["@sentry/node", "npm:6.19.7"],\
             ["@standardnotes/analytics", "npm:1.6.0"],\
             ["@standardnotes/api", "npm:1.1.13"],\
@@ -2626,7 +2684,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
             ["@types/prettyjson", "npm:0.0.29"],\
             ["@types/ua-parser-js", "npm:0.7.36"],\
             ["@types/uuid", "npm:8.3.4"],\
-            ["@typescript-eslint/eslint-plugin", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:5.29.0"],\
+            ["@typescript-eslint/eslint-plugin", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:5.29.0"],\
             ["aws-sdk", "npm:2.1159.0"],\
             ["axios", "npm:0.24.0"],\
             ["bcryptjs", "npm:2.4.3"],\
@@ -2635,19 +2693,19 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
             ["dayjs", "npm:1.11.3"],\
             ["dotenv", "npm:8.2.0"],\
             ["eslint", "npm:8.18.0"],\
-            ["eslint-plugin-prettier", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:4.0.0"],\
+            ["eslint-plugin-prettier", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:4.0.0"],\
             ["express", "npm:4.17.1"],\
             ["inversify", "npm:6.0.1"],\
             ["inversify-express-utils", "npm:6.4.3"],\
             ["ioredis", "npm:5.0.6"],\
-            ["jest", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:28.1.1"],\
+            ["jest", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:28.1.1"],\
             ["mysql2", "npm:2.3.3"],\
             ["newrelic", "npm:8.6.0"],\
             ["nodemon", "npm:2.0.16"],\
             ["otplib", "npm:12.0.1"],\
             ["prettyjson", "npm:1.2.1"],\
             ["reflect-metadata", "npm:0.1.13"],\
-            ["ts-jest", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:28.0.5"],\
+            ["ts-jest", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:28.0.5"],\
             ["typeorm", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:0.3.6"],\
             ["ua-parser-js", "npm:1.0.2"],\
             ["uuid", "npm:8.3.2"],\
@@ -2677,6 +2735,15 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
         }]\
       ]],\
       ["@standardnotes/domain-events", [\
+        ["npm:2.29.0", {\
+          "packageLocation": "./.yarn/cache/@standardnotes-domain-events-npm-2.29.0-13bec3d9a7-1b68999e2a.zip/node_modules/@standardnotes/domain-events/",\
+          "packageDependencies": [\
+            ["@standardnotes/domain-events", "npm:2.29.0"],\
+            ["@standardnotes/auth", "npm:3.19.3"],\
+            ["@standardnotes/features", "npm:1.45.5"]\
+          ],\
+          "linkType": "HARD"\
+        }],\
         ["npm:2.32.2", {\
           "packageLocation": "./.yarn/cache/@standardnotes-domain-events-npm-2.32.2-73adf7a999-54da5fc885.zip/node_modules/@standardnotes/domain-events/",\
           "packageDependencies": [\
@@ -2689,6 +2756,20 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
         }]\
       ]],\
       ["@standardnotes/domain-events-infra", [\
+        ["npm:1.4.127", {\
+          "packageLocation": "./.yarn/cache/@standardnotes-domain-events-infra-npm-1.4.127-18a82f2f72-54e37c296f.zip/node_modules/@standardnotes/domain-events-infra/",\
+          "packageDependencies": [\
+            ["@standardnotes/domain-events-infra", "npm:1.4.127"],\
+            ["@standardnotes/domain-events", "npm:2.32.2"],\
+            ["aws-sdk", "npm:2.1157.0"],\
+            ["ioredis", "npm:4.28.5"],\
+            ["newrelic", "npm:8.14.1"],\
+            ["reflect-metadata", "npm:0.1.13"],\
+            ["sqs-consumer", "virtual:18a82f2f722cf47811304317f79c07fee6ecd8a887f3af2b42c72227f3b982ee05bf525aa8a7d2d20252ed23a1ed39ca99b430ed432b264a8bad79965c0cae5e#npm:5.7.0"],\
+            ["winston", "npm:3.7.2"]\
+          ],\
+          "linkType": "HARD"\
+        }],\
         ["npm:1.5.2", {\
           "packageLocation": "./.yarn/cache/@standardnotes-domain-events-infra-npm-1.5.2-948d77a715-b027dfd329.zip/node_modules/@standardnotes/domain-events-infra/",\
           "packageDependencies": [\
@@ -2698,7 +2779,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
             ["ioredis", "npm:4.28.5"],\
             ["newrelic", "npm:8.14.1"],\
             ["reflect-metadata", "npm:0.1.13"],\
-            ["sqs-consumer", "virtual:948d77a715c2182e00f2764d231af7afb02bf7f3164cf05b0248dc116c0a54912c77c570591f08b5b0dbb55984329e93f5f5704931076ba8f07fc46d4a69d072#npm:5.7.0"],\
+            ["sqs-consumer", "virtual:18a82f2f722cf47811304317f79c07fee6ecd8a887f3af2b42c72227f3b982ee05bf525aa8a7d2d20252ed23a1ed39ca99b430ed432b264a8bad79965c0cae5e#npm:5.7.0"],\
             ["winston", "npm:3.7.2"]\
           ],\
           "linkType": "HARD"\
@@ -2751,27 +2832,27 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
             ["@types/newrelic", "npm:7.0.3"],\
             ["@types/prettyjson", "npm:0.0.29"],\
             ["@types/uuid", "npm:8.3.4"],\
-            ["@typescript-eslint/eslint-plugin", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:5.29.0"],\
+            ["@typescript-eslint/eslint-plugin", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:5.29.0"],\
             ["aws-sdk", "npm:2.1158.0"],\
             ["connect-busboy", "npm:1.0.0"],\
             ["cors", "npm:2.8.5"],\
             ["dayjs", "npm:1.11.3"],\
             ["dotenv", "npm:8.6.0"],\
             ["eslint", "npm:8.18.0"],\
-            ["eslint-plugin-prettier", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:4.0.0"],\
+            ["eslint-plugin-prettier", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:4.0.0"],\
             ["express", "npm:4.18.1"],\
             ["express-winston", "virtual:b442cf0427cc365d1c137f7340f9b81f9b204561afe791a8564ae9590c3a7fc4b5f793aaf8817b946f75a3cb64d03ef8790eb847f8b576b41e700da7b00c240c#npm:4.2.0"],\
             ["helmet", "npm:4.6.0"],\
             ["inversify", "npm:6.0.1"],\
             ["inversify-express-utils", "npm:6.4.3"],\
             ["ioredis", "npm:5.0.6"],\
-            ["jest", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:28.1.1"],\
+            ["jest", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:28.1.1"],\
             ["jsonwebtoken", "npm:8.5.1"],\
             ["newrelic", "npm:7.5.2"],\
             ["nodemon", "npm:2.0.16"],\
             ["prettyjson", "npm:1.2.5"],\
             ["reflect-metadata", "npm:0.1.13"],\
-            ["ts-jest", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:28.0.5"],\
+            ["ts-jest", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:28.0.5"],\
             ["ts-node", "virtual:b442cf0427cc365d1c137f7340f9b81f9b204561afe791a8564ae9590c3a7fc4b5f793aaf8817b946f75a3cb64d03ef8790eb847f8b576b41e700da7b00c240c#npm:10.8.1"],\
             ["uuid", "npm:8.3.2"],\
             ["winston", "npm:3.7.2"]\
@@ -2831,7 +2912,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
           "packageDependencies": [\
             ["@standardnotes/scheduler-server", "workspace:packages/scheduler"],\
             ["@newrelic/native-metrics", "npm:7.0.2"],\
-            ["@newrelic/winston-enricher", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:2.1.2"],\
+            ["@newrelic/winston-enricher", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:2.1.2"],\
             ["@standardnotes/common", "npm:1.23.0"],\
             ["@standardnotes/domain-events", "npm:2.32.2"],\
             ["@standardnotes/domain-events-infra", "npm:1.5.2"],\
@@ -2848,7 +2929,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
             ["eslint-plugin-prettier", "virtual:16bfd8597041deb71e4581ea0755edd4dcd1b09b8ab14bfbbf5e4d5ca6b5d47ed7fbe2a25cdf57fcbb8e092c30b6beb93d2e7533f9e31c5dc62f7f0e487d1e4b#npm:4.0.0"],\
             ["inversify", "npm:5.0.5"],\
             ["ioredis", "npm:5.0.6"],\
-            ["jest", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:28.1.1"],\
+            ["jest", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:28.1.1"],\
             ["mysql2", "npm:2.3.3"],\
             ["newrelic", "npm:8.6.0"],\
             ["reflect-metadata", "npm:0.1.13"],\
@@ -2928,7 +3009,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
           "packageDependencies": [\
             ["@standardnotes/syncing-server", "workspace:packages/syncing-server"],\
             ["@newrelic/native-metrics", "npm:7.0.2"],\
-            ["@newrelic/winston-enricher", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:2.1.2"],\
+            ["@newrelic/winston-enricher", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:2.1.2"],\
             ["@sentry/node", "npm:6.19.7"],\
             ["@standardnotes/analytics", "npm:1.6.0"],\
             ["@standardnotes/auth", "npm:3.19.3"],\
@@ -2950,26 +3031,26 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
             ["@types/prettyjson", "npm:0.0.29"],\
             ["@types/ua-parser-js", "npm:0.7.36"],\
             ["@types/uuid", "npm:8.3.4"],\
-            ["@typescript-eslint/eslint-plugin", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:5.29.0"],\
+            ["@typescript-eslint/eslint-plugin", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:5.29.0"],\
             ["aws-sdk", "npm:2.1159.0"],\
             ["axios", "npm:0.24.0"],\
             ["cors", "npm:2.8.5"],\
             ["dotenv", "npm:8.2.0"],\
             ["eslint", "npm:8.18.0"],\
-            ["eslint-plugin-prettier", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:4.0.0"],\
+            ["eslint-plugin-prettier", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:4.0.0"],\
             ["express", "npm:4.17.1"],\
             ["helmet", "npm:4.3.1"],\
             ["inversify", "npm:6.0.1"],\
             ["inversify-express-utils", "npm:6.4.3"],\
             ["ioredis", "npm:5.0.6"],\
-            ["jest", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:28.1.1"],\
+            ["jest", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:28.1.1"],\
             ["jsonwebtoken", "npm:8.5.1"],\
             ["mysql2", "npm:2.3.3"],\
             ["newrelic", "npm:8.6.0"],\
             ["nodemon", "npm:2.0.7"],\
             ["prettyjson", "npm:1.2.1"],\
             ["reflect-metadata", "npm:0.1.13"],\
-            ["ts-jest", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:28.0.5"],\
+            ["ts-jest", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:28.0.5"],\
             ["typeorm", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:0.3.6"],\
             ["ua-parser-js", "npm:1.0.2"],\
             ["uuid", "npm:8.3.2"],\
@@ -3513,24 +3594,24 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
           ],\
           "linkType": "SOFT"\
         }],\
-        ["virtual:16bfd8597041deb71e4581ea0755edd4dcd1b09b8ab14bfbbf5e4d5ca6b5d47ed7fbe2a25cdf57fcbb8e092c30b6beb93d2e7533f9e31c5dc62f7f0e487d1e4b#npm:5.29.0", {\
-          "packageLocation": "./.yarn/__virtual__/@typescript-eslint-eslint-plugin-virtual-f8d5e1f46d/0/cache/@typescript-eslint-eslint-plugin-npm-5.29.0-d7e482bb3e-b1022a640f.zip/node_modules/@typescript-eslint/eslint-plugin/",\
+        ["virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:5.29.0", {\
+          "packageLocation": "./.yarn/__virtual__/@typescript-eslint-eslint-plugin-virtual-d2330b8caf/0/cache/@typescript-eslint-eslint-plugin-npm-5.29.0-d7e482bb3e-b1022a640f.zip/node_modules/@typescript-eslint/eslint-plugin/",\
           "packageDependencies": [\
-            ["@typescript-eslint/eslint-plugin", "virtual:16bfd8597041deb71e4581ea0755edd4dcd1b09b8ab14bfbbf5e4d5ca6b5d47ed7fbe2a25cdf57fcbb8e092c30b6beb93d2e7533f9e31c5dc62f7f0e487d1e4b#npm:5.29.0"],\
+            ["@typescript-eslint/eslint-plugin", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:5.29.0"],\
             ["@types/eslint", null],\
             ["@types/typescript", null],\
             ["@types/typescript-eslint__parser", null],\
             ["@typescript-eslint/parser", null],\
             ["@typescript-eslint/scope-manager", "npm:5.29.0"],\
-            ["@typescript-eslint/type-utils", "virtual:f8d5e1f46dbb2f1fb352e8d15f1237589f03161f87569a6446ffa325c842024c20e3b7f196872650fbbdc62125c711d99dd1c2ba271f15e9b316292a2dec51bc#npm:5.29.0"],\
-            ["@typescript-eslint/utils", "virtual:f8d5e1f46dbb2f1fb352e8d15f1237589f03161f87569a6446ffa325c842024c20e3b7f196872650fbbdc62125c711d99dd1c2ba271f15e9b316292a2dec51bc#npm:5.29.0"],\
+            ["@typescript-eslint/type-utils", "virtual:d2330b8caf9d1ed905d5d037b08afe5d2dcb2220d795f2d96b3dd514824e8aefae721c334dbd0cd8d53c639ff2c9ad893d3d83b1092b2db05593aa9dc8e59994#npm:5.29.0"],\
+            ["@typescript-eslint/utils", "virtual:d2330b8caf9d1ed905d5d037b08afe5d2dcb2220d795f2d96b3dd514824e8aefae721c334dbd0cd8d53c639ff2c9ad893d3d83b1092b2db05593aa9dc8e59994#npm:5.29.0"],\
             ["debug", "virtual:b86a9fb34323a98c6519528ed55faa0d9b44ca8879307c0b29aa384bde47ff59a7d0c9051b31246f14521dfb71ba3c5d6d0b35c29fffc17bf875aa6ad977d9e8#npm:4.3.4"],\
-            ["eslint", null],\
+            ["eslint", "npm:8.18.0"],\
             ["functional-red-black-tree", "npm:1.0.1"],\
             ["ignore", "npm:5.2.0"],\
             ["regexpp", "npm:3.2.0"],\
             ["semver", "npm:7.3.7"],\
-            ["tsutils", "virtual:e64d2841693653abb2dee666d19406912f5e913a8081a709c081d9877d2f39987ff853b7cd736901a2df59af98328f7249f3db0da01abf060cf1d858d4d4e43b#npm:3.21.0"],\
+            ["tsutils", "virtual:d2330b8caf9d1ed905d5d037b08afe5d2dcb2220d795f2d96b3dd514824e8aefae721c334dbd0cd8d53c639ff2c9ad893d3d83b1092b2db05593aa9dc8e59994#npm:3.21.0"],\
             ["typescript", null]\
           ],\
           "packagePeers": [\
@@ -3543,24 +3624,24 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
           ],\
           "linkType": "HARD"\
         }],\
-        ["virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:5.29.0", {\
-          "packageLocation": "./.yarn/__virtual__/@typescript-eslint-eslint-plugin-virtual-e64d284169/0/cache/@typescript-eslint-eslint-plugin-npm-5.29.0-d7e482bb3e-b1022a640f.zip/node_modules/@typescript-eslint/eslint-plugin/",\
+        ["virtual:16bfd8597041deb71e4581ea0755edd4dcd1b09b8ab14bfbbf5e4d5ca6b5d47ed7fbe2a25cdf57fcbb8e092c30b6beb93d2e7533f9e31c5dc62f7f0e487d1e4b#npm:5.29.0", {\
+          "packageLocation": "./.yarn/__virtual__/@typescript-eslint-eslint-plugin-virtual-f8d5e1f46d/0/cache/@typescript-eslint-eslint-plugin-npm-5.29.0-d7e482bb3e-b1022a640f.zip/node_modules/@typescript-eslint/eslint-plugin/",\
           "packageDependencies": [\
-            ["@typescript-eslint/eslint-plugin", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:5.29.0"],\
+            ["@typescript-eslint/eslint-plugin", "virtual:16bfd8597041deb71e4581ea0755edd4dcd1b09b8ab14bfbbf5e4d5ca6b5d47ed7fbe2a25cdf57fcbb8e092c30b6beb93d2e7533f9e31c5dc62f7f0e487d1e4b#npm:5.29.0"],\
             ["@types/eslint", null],\
             ["@types/typescript", null],\
             ["@types/typescript-eslint__parser", null],\
             ["@typescript-eslint/parser", null],\
             ["@typescript-eslint/scope-manager", "npm:5.29.0"],\
-            ["@typescript-eslint/type-utils", "virtual:e64d2841693653abb2dee666d19406912f5e913a8081a709c081d9877d2f39987ff853b7cd736901a2df59af98328f7249f3db0da01abf060cf1d858d4d4e43b#npm:5.29.0"],\
-            ["@typescript-eslint/utils", "virtual:e64d2841693653abb2dee666d19406912f5e913a8081a709c081d9877d2f39987ff853b7cd736901a2df59af98328f7249f3db0da01abf060cf1d858d4d4e43b#npm:5.29.0"],\
+            ["@typescript-eslint/type-utils", "virtual:f8d5e1f46dbb2f1fb352e8d15f1237589f03161f87569a6446ffa325c842024c20e3b7f196872650fbbdc62125c711d99dd1c2ba271f15e9b316292a2dec51bc#npm:5.29.0"],\
+            ["@typescript-eslint/utils", "virtual:f8d5e1f46dbb2f1fb352e8d15f1237589f03161f87569a6446ffa325c842024c20e3b7f196872650fbbdc62125c711d99dd1c2ba271f15e9b316292a2dec51bc#npm:5.29.0"],\
             ["debug", "virtual:b86a9fb34323a98c6519528ed55faa0d9b44ca8879307c0b29aa384bde47ff59a7d0c9051b31246f14521dfb71ba3c5d6d0b35c29fffc17bf875aa6ad977d9e8#npm:4.3.4"],\
-            ["eslint", "npm:8.18.0"],\
+            ["eslint", null],\
             ["functional-red-black-tree", "npm:1.0.1"],\
             ["ignore", "npm:5.2.0"],\
             ["regexpp", "npm:3.2.0"],\
             ["semver", "npm:7.3.7"],\
-            ["tsutils", "virtual:e64d2841693653abb2dee666d19406912f5e913a8081a709c081d9877d2f39987ff853b7cd736901a2df59af98328f7249f3db0da01abf060cf1d858d4d4e43b#npm:3.21.0"],\
+            ["tsutils", "virtual:d2330b8caf9d1ed905d5d037b08afe5d2dcb2220d795f2d96b3dd514824e8aefae721c334dbd0cd8d53c639ff2c9ad893d3d83b1092b2db05593aa9dc8e59994#npm:3.21.0"],\
             ["typescript", null]\
           ],\
           "packagePeers": [\
@@ -3589,7 +3670,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
             ["ignore", "npm:5.2.0"],\
             ["regexpp", "npm:3.2.0"],\
             ["semver", "npm:7.3.7"],\
-            ["tsutils", "virtual:e64d2841693653abb2dee666d19406912f5e913a8081a709c081d9877d2f39987ff853b7cd736901a2df59af98328f7249f3db0da01abf060cf1d858d4d4e43b#npm:3.21.0"],\
+            ["tsutils", "virtual:d2330b8caf9d1ed905d5d037b08afe5d2dcb2220d795f2d96b3dd514824e8aefae721c334dbd0cd8d53c639ff2c9ad893d3d83b1092b2db05593aa9dc8e59994#npm:3.21.0"],\
             ["typescript", null]\
           ],\
           "packagePeers": [\
@@ -3717,16 +3798,16 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
           ],\
           "linkType": "SOFT"\
         }],\
-        ["virtual:e64d2841693653abb2dee666d19406912f5e913a8081a709c081d9877d2f39987ff853b7cd736901a2df59af98328f7249f3db0da01abf060cf1d858d4d4e43b#npm:5.29.0", {\
-          "packageLocation": "./.yarn/__virtual__/@typescript-eslint-type-utils-virtual-96dd1bc160/0/cache/@typescript-eslint-type-utils-npm-5.29.0-063d15676f-686b8ff05a.zip/node_modules/@typescript-eslint/type-utils/",\
+        ["virtual:d2330b8caf9d1ed905d5d037b08afe5d2dcb2220d795f2d96b3dd514824e8aefae721c334dbd0cd8d53c639ff2c9ad893d3d83b1092b2db05593aa9dc8e59994#npm:5.29.0", {\
+          "packageLocation": "./.yarn/__virtual__/@typescript-eslint-type-utils-virtual-2a27889b50/0/cache/@typescript-eslint-type-utils-npm-5.29.0-063d15676f-686b8ff05a.zip/node_modules/@typescript-eslint/type-utils/",\
           "packageDependencies": [\
-            ["@typescript-eslint/type-utils", "virtual:e64d2841693653abb2dee666d19406912f5e913a8081a709c081d9877d2f39987ff853b7cd736901a2df59af98328f7249f3db0da01abf060cf1d858d4d4e43b#npm:5.29.0"],\
+            ["@typescript-eslint/type-utils", "virtual:d2330b8caf9d1ed905d5d037b08afe5d2dcb2220d795f2d96b3dd514824e8aefae721c334dbd0cd8d53c639ff2c9ad893d3d83b1092b2db05593aa9dc8e59994#npm:5.29.0"],\
             ["@types/eslint", null],\
             ["@types/typescript", null],\
-            ["@typescript-eslint/utils", "virtual:e64d2841693653abb2dee666d19406912f5e913a8081a709c081d9877d2f39987ff853b7cd736901a2df59af98328f7249f3db0da01abf060cf1d858d4d4e43b#npm:5.29.0"],\
+            ["@typescript-eslint/utils", "virtual:d2330b8caf9d1ed905d5d037b08afe5d2dcb2220d795f2d96b3dd514824e8aefae721c334dbd0cd8d53c639ff2c9ad893d3d83b1092b2db05593aa9dc8e59994#npm:5.29.0"],\
             ["debug", "virtual:b86a9fb34323a98c6519528ed55faa0d9b44ca8879307c0b29aa384bde47ff59a7d0c9051b31246f14521dfb71ba3c5d6d0b35c29fffc17bf875aa6ad977d9e8#npm:4.3.4"],\
             ["eslint", "npm:8.18.0"],\
-            ["tsutils", "virtual:e64d2841693653abb2dee666d19406912f5e913a8081a709c081d9877d2f39987ff853b7cd736901a2df59af98328f7249f3db0da01abf060cf1d858d4d4e43b#npm:3.21.0"],\
+            ["tsutils", "virtual:d2330b8caf9d1ed905d5d037b08afe5d2dcb2220d795f2d96b3dd514824e8aefae721c334dbd0cd8d53c639ff2c9ad893d3d83b1092b2db05593aa9dc8e59994#npm:3.21.0"],\
             ["typescript", null]\
           ],\
           "packagePeers": [\
@@ -3746,7 +3827,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
             ["@typescript-eslint/utils", "virtual:f8d5e1f46dbb2f1fb352e8d15f1237589f03161f87569a6446ffa325c842024c20e3b7f196872650fbbdc62125c711d99dd1c2ba271f15e9b316292a2dec51bc#npm:5.29.0"],\
             ["debug", "virtual:b86a9fb34323a98c6519528ed55faa0d9b44ca8879307c0b29aa384bde47ff59a7d0c9051b31246f14521dfb71ba3c5d6d0b35c29fffc17bf875aa6ad977d9e8#npm:4.3.4"],\
             ["eslint", null],\
-            ["tsutils", "virtual:e64d2841693653abb2dee666d19406912f5e913a8081a709c081d9877d2f39987ff853b7cd736901a2df59af98328f7249f3db0da01abf060cf1d858d4d4e43b#npm:3.21.0"],\
+            ["tsutils", "virtual:d2330b8caf9d1ed905d5d037b08afe5d2dcb2220d795f2d96b3dd514824e8aefae721c334dbd0cd8d53c639ff2c9ad893d3d83b1092b2db05593aa9dc8e59994#npm:3.21.0"],\
             ["typescript", null]\
           ],\
           "packagePeers": [\
@@ -3800,7 +3881,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
             ["globby", "npm:11.1.0"],\
             ["is-glob", "npm:4.0.3"],\
             ["semver", "npm:7.3.7"],\
-            ["tsutils", "virtual:e64d2841693653abb2dee666d19406912f5e913a8081a709c081d9877d2f39987ff853b7cd736901a2df59af98328f7249f3db0da01abf060cf1d858d4d4e43b#npm:3.21.0"],\
+            ["tsutils", "virtual:d2330b8caf9d1ed905d5d037b08afe5d2dcb2220d795f2d96b3dd514824e8aefae721c334dbd0cd8d53c639ff2c9ad893d3d83b1092b2db05593aa9dc8e59994#npm:3.21.0"],\
             ["typescript", null]\
           ],\
           "packagePeers": [\
@@ -3829,10 +3910,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
           ],\
           "linkType": "HARD"\
         }],\
-        ["virtual:4ec458b53cfcb38d153394fe4d0300908a12ce721ae6026f1e2d7bbe8409ed98079b29d9688a9eb93463ace5dbaac7d454b12c4582b1cd0b1d8210588cf0cb1c#npm:5.29.0", {\
-          "packageLocation": "./.yarn/__virtual__/@typescript-eslint-typescript-estree-virtual-8de7b9cb0d/0/cache/@typescript-eslint-typescript-estree-npm-5.29.0-f23de2ab5c-b91107a9fc.zip/node_modules/@typescript-eslint/typescript-estree/",\
+        ["virtual:5355269a141f9a806d08de8974384aa45476d01ec51fc7301c8384e52bbf36be0eab945f0602a4df4c7737fa267f81331777eaeb3d1de352f83549caee258ae1#npm:5.29.0", {\
+          "packageLocation": "./.yarn/__virtual__/@typescript-eslint-typescript-estree-virtual-76f89dbc29/0/cache/@typescript-eslint-typescript-estree-npm-5.29.0-f23de2ab5c-b91107a9fc.zip/node_modules/@typescript-eslint/typescript-estree/",\
           "packageDependencies": [\
-            ["@typescript-eslint/typescript-estree", "virtual:4ec458b53cfcb38d153394fe4d0300908a12ce721ae6026f1e2d7bbe8409ed98079b29d9688a9eb93463ace5dbaac7d454b12c4582b1cd0b1d8210588cf0cb1c#npm:5.29.0"],\
+            ["@typescript-eslint/typescript-estree", "virtual:5355269a141f9a806d08de8974384aa45476d01ec51fc7301c8384e52bbf36be0eab945f0602a4df4c7737fa267f81331777eaeb3d1de352f83549caee258ae1#npm:5.29.0"],\
             ["@types/typescript", null],\
             ["@typescript-eslint/types", "npm:5.29.0"],\
             ["@typescript-eslint/visitor-keys", "npm:5.29.0"],\
@@ -3840,7 +3921,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
             ["globby", "npm:11.1.0"],\
             ["is-glob", "npm:4.0.3"],\
             ["semver", "npm:7.3.7"],\
-            ["tsutils", "virtual:e64d2841693653abb2dee666d19406912f5e913a8081a709c081d9877d2f39987ff853b7cd736901a2df59af98328f7249f3db0da01abf060cf1d858d4d4e43b#npm:3.21.0"],\
+            ["tsutils", "virtual:d2330b8caf9d1ed905d5d037b08afe5d2dcb2220d795f2d96b3dd514824e8aefae721c334dbd0cd8d53c639ff2c9ad893d3d83b1092b2db05593aa9dc8e59994#npm:3.21.0"],\
             ["typescript", null]\
           ],\
           "packagePeers": [\
@@ -3858,15 +3939,15 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
           ],\
           "linkType": "SOFT"\
         }],\
-        ["virtual:e64d2841693653abb2dee666d19406912f5e913a8081a709c081d9877d2f39987ff853b7cd736901a2df59af98328f7249f3db0da01abf060cf1d858d4d4e43b#npm:5.29.0", {\
-          "packageLocation": "./.yarn/__virtual__/@typescript-eslint-utils-virtual-4ec458b53c/0/cache/@typescript-eslint-utils-npm-5.29.0-f3bfc5f3f3-216f51fb9c.zip/node_modules/@typescript-eslint/utils/",\
+        ["virtual:d2330b8caf9d1ed905d5d037b08afe5d2dcb2220d795f2d96b3dd514824e8aefae721c334dbd0cd8d53c639ff2c9ad893d3d83b1092b2db05593aa9dc8e59994#npm:5.29.0", {\
+          "packageLocation": "./.yarn/__virtual__/@typescript-eslint-utils-virtual-5355269a14/0/cache/@typescript-eslint-utils-npm-5.29.0-f3bfc5f3f3-216f51fb9c.zip/node_modules/@typescript-eslint/utils/",\
           "packageDependencies": [\
-            ["@typescript-eslint/utils", "virtual:e64d2841693653abb2dee666d19406912f5e913a8081a709c081d9877d2f39987ff853b7cd736901a2df59af98328f7249f3db0da01abf060cf1d858d4d4e43b#npm:5.29.0"],\
+            ["@typescript-eslint/utils", "virtual:d2330b8caf9d1ed905d5d037b08afe5d2dcb2220d795f2d96b3dd514824e8aefae721c334dbd0cd8d53c639ff2c9ad893d3d83b1092b2db05593aa9dc8e59994#npm:5.29.0"],\
             ["@types/eslint", null],\
             ["@types/json-schema", "npm:7.0.11"],\
             ["@typescript-eslint/scope-manager", "npm:5.29.0"],\
             ["@typescript-eslint/types", "npm:5.29.0"],\
-            ["@typescript-eslint/typescript-estree", "virtual:4ec458b53cfcb38d153394fe4d0300908a12ce721ae6026f1e2d7bbe8409ed98079b29d9688a9eb93463ace5dbaac7d454b12c4582b1cd0b1d8210588cf0cb1c#npm:5.29.0"],\
+            ["@typescript-eslint/typescript-estree", "virtual:5355269a141f9a806d08de8974384aa45476d01ec51fc7301c8384e52bbf36be0eab945f0602a4df4c7737fa267f81331777eaeb3d1de352f83549caee258ae1#npm:5.29.0"],\
             ["eslint", "npm:8.18.0"],\
             ["eslint-scope", "npm:5.1.1"],\
             ["eslint-utils", "virtual:3b3bfb190f25ed01591b1d51c8e6a15e818ab97d9cabea5c63912afc819a8f6e3ad395aaf338cd170314411b04e35eec5c8cff33dfa644476d292dcf2c5354d1#npm:3.0.0"]\
@@ -3885,7 +3966,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
             ["@types/json-schema", "npm:7.0.11"],\
             ["@typescript-eslint/scope-manager", "npm:5.29.0"],\
             ["@typescript-eslint/types", "npm:5.29.0"],\
-            ["@typescript-eslint/typescript-estree", "virtual:4ec458b53cfcb38d153394fe4d0300908a12ce721ae6026f1e2d7bbe8409ed98079b29d9688a9eb93463ace5dbaac7d454b12c4582b1cd0b1d8210588cf0cb1c#npm:5.29.0"],\
+            ["@typescript-eslint/typescript-estree", "virtual:5355269a141f9a806d08de8974384aa45476d01ec51fc7301c8384e52bbf36be0eab945f0602a4df4c7737fa267f81331777eaeb3d1de352f83549caee258ae1#npm:5.29.0"],\
             ["eslint", null],\
             ["eslint-scope", "npm:5.1.1"],\
             ["eslint-utils", "virtual:3b1d487b65ac14c3c2f5d6292c3e4b93bf25216a88a2d253428f98942e01532ac4933ee30564874cec0a0bb5aea3ee613d7494705e42eed4a2106f8ac0a03f97#npm:3.0.0"]\
@@ -4297,6 +4378,22 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
             ["xml2js", "npm:0.4.19"]\
           ],\
           "linkType": "HARD"\
+        }],\
+        ["npm:2.1160.0", {\
+          "packageLocation": "./.yarn/cache/aws-sdk-npm-2.1160.0-1a3db600b7-b95647d4de.zip/node_modules/aws-sdk/",\
+          "packageDependencies": [\
+            ["aws-sdk", "npm:2.1160.0"],\
+            ["buffer", "npm:4.9.2"],\
+            ["events", "npm:1.1.1"],\
+            ["ieee754", "npm:1.1.13"],\
+            ["jmespath", "npm:0.16.0"],\
+            ["querystring", "npm:0.2.0"],\
+            ["sax", "npm:1.2.1"],\
+            ["url", "npm:0.10.3"],\
+            ["uuid", "npm:8.0.0"],\
+            ["xml2js", "npm:0.4.19"]\
+          ],\
+          "linkType": "HARD"\
         }]\
       ]],\
       ["axios", [\
@@ -4317,15 +4414,15 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
           ],\
           "linkType": "SOFT"\
         }],\
-        ["virtual:805c813b6f046618cef5c7d6c026d202467ce267579e0c7a252be4f063439bc6f090ab5b924f50d7ae022b220d8e89e00ef15869e26244774ec68ef480e4e54d#npm:28.1.1", {\
-          "packageLocation": "./.yarn/__virtual__/babel-jest-virtual-419439314f/0/cache/babel-jest-npm-28.1.1-a0706ab037-9c7c7f6006.zip/node_modules/babel-jest/",\
+        ["virtual:32b5269fd2e54a6fa7239e0777f3768bd33c5a094884dd5492350113f2bec8947944fe0156c37c044965c85742064fae363a3a7138decb195966e011bb7f7d4f#npm:28.1.1", {\
+          "packageLocation": "./.yarn/__virtual__/babel-jest-virtual-08d3591870/0/cache/babel-jest-npm-28.1.1-a0706ab037-9c7c7f6006.zip/node_modules/babel-jest/",\
           "packageDependencies": [\
-            ["babel-jest", "virtual:805c813b6f046618cef5c7d6c026d202467ce267579e0c7a252be4f063439bc6f090ab5b924f50d7ae022b220d8e89e00ef15869e26244774ec68ef480e4e54d#npm:28.1.1"],\
+            ["babel-jest", "virtual:32b5269fd2e54a6fa7239e0777f3768bd33c5a094884dd5492350113f2bec8947944fe0156c37c044965c85742064fae363a3a7138decb195966e011bb7f7d4f#npm:28.1.1"],\
             ["@babel/core", "npm:7.18.5"],\
             ["@jest/transform", "npm:28.1.1"],\
             ["@types/babel__core", "npm:7.1.19"],\
             ["babel-plugin-istanbul", "npm:6.1.1"],\
-            ["babel-preset-jest", "virtual:419439314f6ac7e6aeb104f74d9bd1fb754b552a3112b86e2807390519b56dbd6a88f32cff6e239d59a2670b389ec32d1afc7812be06ebe4cb1eeb9c2c58cf9e#npm:28.1.1"],\
+            ["babel-preset-jest", "virtual:08d35918703ec0bc2fb72b89407cc8430c63eb749ffe2bff641418cce72be4aab0afe750c610e41ee52e9bd2c8d7e721847c3791d36317060f0620130926424e#npm:28.1.1"],\
             ["chalk", "npm:4.1.2"],\
             ["graceful-fs", "npm:4.2.10"],\
             ["slash", "npm:3.0.0"]\
@@ -4371,6 +4468,31 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
           ],\
           "linkType": "SOFT"\
         }],\
+        ["virtual:081cd5e09087a97f33e6c2157b2f04624b270420cbd295805f8d59379d1fdc60eb969f610d8b11a66179e64b76fb08cb87d36cb6f67d17466aa40dc8ab796225#npm:1.0.1", {\
+          "packageLocation": "./.yarn/__virtual__/babel-preset-current-node-syntax-virtual-c373874e1b/0/cache/babel-preset-current-node-syntax-npm-1.0.1-849ec71e32-d118c27424.zip/node_modules/babel-preset-current-node-syntax/",\
+          "packageDependencies": [\
+            ["babel-preset-current-node-syntax", "virtual:081cd5e09087a97f33e6c2157b2f04624b270420cbd295805f8d59379d1fdc60eb969f610d8b11a66179e64b76fb08cb87d36cb6f67d17466aa40dc8ab796225#npm:1.0.1"],\
+            ["@babel/core", "npm:7.18.5"],\
+            ["@babel/plugin-syntax-async-generators", "virtual:c373874e1baa9d76ffb6ce2109646b13db6b2b433728eac72e69f1da2ca3718a077c7549e49925b0fa06952690278e6a36ceca4ebe492de934ac9dea98343dd7#npm:7.8.4"],\
+            ["@babel/plugin-syntax-bigint", "virtual:c373874e1baa9d76ffb6ce2109646b13db6b2b433728eac72e69f1da2ca3718a077c7549e49925b0fa06952690278e6a36ceca4ebe492de934ac9dea98343dd7#npm:7.8.3"],\
+            ["@babel/plugin-syntax-class-properties", "virtual:c373874e1baa9d76ffb6ce2109646b13db6b2b433728eac72e69f1da2ca3718a077c7549e49925b0fa06952690278e6a36ceca4ebe492de934ac9dea98343dd7#npm:7.12.13"],\
+            ["@babel/plugin-syntax-import-meta", "virtual:c373874e1baa9d76ffb6ce2109646b13db6b2b433728eac72e69f1da2ca3718a077c7549e49925b0fa06952690278e6a36ceca4ebe492de934ac9dea98343dd7#npm:7.10.4"],\
+            ["@babel/plugin-syntax-json-strings", "virtual:c373874e1baa9d76ffb6ce2109646b13db6b2b433728eac72e69f1da2ca3718a077c7549e49925b0fa06952690278e6a36ceca4ebe492de934ac9dea98343dd7#npm:7.8.3"],\
+            ["@babel/plugin-syntax-logical-assignment-operators", "virtual:c373874e1baa9d76ffb6ce2109646b13db6b2b433728eac72e69f1da2ca3718a077c7549e49925b0fa06952690278e6a36ceca4ebe492de934ac9dea98343dd7#npm:7.10.4"],\
+            ["@babel/plugin-syntax-nullish-coalescing-operator", "virtual:c373874e1baa9d76ffb6ce2109646b13db6b2b433728eac72e69f1da2ca3718a077c7549e49925b0fa06952690278e6a36ceca4ebe492de934ac9dea98343dd7#npm:7.8.3"],\
+            ["@babel/plugin-syntax-numeric-separator", "virtual:c373874e1baa9d76ffb6ce2109646b13db6b2b433728eac72e69f1da2ca3718a077c7549e49925b0fa06952690278e6a36ceca4ebe492de934ac9dea98343dd7#npm:7.10.4"],\
+            ["@babel/plugin-syntax-object-rest-spread", "virtual:c373874e1baa9d76ffb6ce2109646b13db6b2b433728eac72e69f1da2ca3718a077c7549e49925b0fa06952690278e6a36ceca4ebe492de934ac9dea98343dd7#npm:7.8.3"],\
+            ["@babel/plugin-syntax-optional-catch-binding", "virtual:c373874e1baa9d76ffb6ce2109646b13db6b2b433728eac72e69f1da2ca3718a077c7549e49925b0fa06952690278e6a36ceca4ebe492de934ac9dea98343dd7#npm:7.8.3"],\
+            ["@babel/plugin-syntax-optional-chaining", "virtual:c373874e1baa9d76ffb6ce2109646b13db6b2b433728eac72e69f1da2ca3718a077c7549e49925b0fa06952690278e6a36ceca4ebe492de934ac9dea98343dd7#npm:7.8.3"],\
+            ["@babel/plugin-syntax-top-level-await", "virtual:c373874e1baa9d76ffb6ce2109646b13db6b2b433728eac72e69f1da2ca3718a077c7549e49925b0fa06952690278e6a36ceca4ebe492de934ac9dea98343dd7#npm:7.14.5"],\
+            ["@types/babel__core", "npm:7.1.19"]\
+          ],\
+          "packagePeers": [\
+            "@babel/core",\
+            "@types/babel__core"\
+          ],\
+          "linkType": "HARD"\
+        }],\
         ["virtual:7ff9a9a22630d18bc8c20eec37b86b4c191fbcee5349c62dbf8ba14d95b3502ae4cb63cce8e26089a0dd1b269b70fad4ce808ff97d3255679417f5177f7bef0e#npm:1.0.1", {\
           "packageLocation": "./.yarn/__virtual__/babel-preset-current-node-syntax-virtual-fcabcac42b/0/cache/babel-preset-current-node-syntax-npm-1.0.1-849ec71e32-d118c27424.zip/node_modules/babel-preset-current-node-syntax/",\
           "packageDependencies": [\
@@ -4395,31 +4517,6 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
             "@types/babel__core"\
           ],\
           "linkType": "HARD"\
-        }],\
-        ["virtual:a21946f32fecc6d2a4f39c804bc6851b8c98cf267db2bb0a25b0f443fe3cf1ff67012036ab014b3ec1309cc1f0a5678c35acb443e7d8c8a0d3c29071288e53d7#npm:1.0.1", {\
-          "packageLocation": "./.yarn/__virtual__/babel-preset-current-node-syntax-virtual-511f18ec47/0/cache/babel-preset-current-node-syntax-npm-1.0.1-849ec71e32-d118c27424.zip/node_modules/babel-preset-current-node-syntax/",\
-          "packageDependencies": [\
-            ["babel-preset-current-node-syntax", "virtual:a21946f32fecc6d2a4f39c804bc6851b8c98cf267db2bb0a25b0f443fe3cf1ff67012036ab014b3ec1309cc1f0a5678c35acb443e7d8c8a0d3c29071288e53d7#npm:1.0.1"],\
-            ["@babel/core", "npm:7.18.5"],\
-            ["@babel/plugin-syntax-async-generators", "virtual:511f18ec4797ab42da93993c808942012470709446ef400bc600a66a60e5e382e8081da234db4fe66d41d38d9212735169fb3c604c828d1ff8dc854506ed7b48#npm:7.8.4"],\
-            ["@babel/plugin-syntax-bigint", "virtual:511f18ec4797ab42da93993c808942012470709446ef400bc600a66a60e5e382e8081da234db4fe66d41d38d9212735169fb3c604c828d1ff8dc854506ed7b48#npm:7.8.3"],\
-            ["@babel/plugin-syntax-class-properties", "virtual:511f18ec4797ab42da93993c808942012470709446ef400bc600a66a60e5e382e8081da234db4fe66d41d38d9212735169fb3c604c828d1ff8dc854506ed7b48#npm:7.12.13"],\
-            ["@babel/plugin-syntax-import-meta", "virtual:511f18ec4797ab42da93993c808942012470709446ef400bc600a66a60e5e382e8081da234db4fe66d41d38d9212735169fb3c604c828d1ff8dc854506ed7b48#npm:7.10.4"],\
-            ["@babel/plugin-syntax-json-strings", "virtual:511f18ec4797ab42da93993c808942012470709446ef400bc600a66a60e5e382e8081da234db4fe66d41d38d9212735169fb3c604c828d1ff8dc854506ed7b48#npm:7.8.3"],\
-            ["@babel/plugin-syntax-logical-assignment-operators", "virtual:511f18ec4797ab42da93993c808942012470709446ef400bc600a66a60e5e382e8081da234db4fe66d41d38d9212735169fb3c604c828d1ff8dc854506ed7b48#npm:7.10.4"],\
-            ["@babel/plugin-syntax-nullish-coalescing-operator", "virtual:511f18ec4797ab42da93993c808942012470709446ef400bc600a66a60e5e382e8081da234db4fe66d41d38d9212735169fb3c604c828d1ff8dc854506ed7b48#npm:7.8.3"],\
-            ["@babel/plugin-syntax-numeric-separator", "virtual:511f18ec4797ab42da93993c808942012470709446ef400bc600a66a60e5e382e8081da234db4fe66d41d38d9212735169fb3c604c828d1ff8dc854506ed7b48#npm:7.10.4"],\
-            ["@babel/plugin-syntax-object-rest-spread", "virtual:511f18ec4797ab42da93993c808942012470709446ef400bc600a66a60e5e382e8081da234db4fe66d41d38d9212735169fb3c604c828d1ff8dc854506ed7b48#npm:7.8.3"],\
-            ["@babel/plugin-syntax-optional-catch-binding", "virtual:511f18ec4797ab42da93993c808942012470709446ef400bc600a66a60e5e382e8081da234db4fe66d41d38d9212735169fb3c604c828d1ff8dc854506ed7b48#npm:7.8.3"],\
-            ["@babel/plugin-syntax-optional-chaining", "virtual:511f18ec4797ab42da93993c808942012470709446ef400bc600a66a60e5e382e8081da234db4fe66d41d38d9212735169fb3c604c828d1ff8dc854506ed7b48#npm:7.8.3"],\
-            ["@babel/plugin-syntax-top-level-await", "virtual:511f18ec4797ab42da93993c808942012470709446ef400bc600a66a60e5e382e8081da234db4fe66d41d38d9212735169fb3c604c828d1ff8dc854506ed7b48#npm:7.14.5"],\
-            ["@types/babel__core", "npm:7.1.19"]\
-          ],\
-          "packagePeers": [\
-            "@babel/core",\
-            "@types/babel__core"\
-          ],\
-          "linkType": "HARD"\
         }]\
       ]],\
       ["babel-preset-jest", [\
@@ -4430,14 +4527,14 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
           ],\
           "linkType": "SOFT"\
         }],\
-        ["virtual:419439314f6ac7e6aeb104f74d9bd1fb754b552a3112b86e2807390519b56dbd6a88f32cff6e239d59a2670b389ec32d1afc7812be06ebe4cb1eeb9c2c58cf9e#npm:28.1.1", {\
-          "packageLocation": "./.yarn/__virtual__/babel-preset-jest-virtual-a21946f32f/0/cache/babel-preset-jest-npm-28.1.1-05a1e38dd1-c581a81967.zip/node_modules/babel-preset-jest/",\
+        ["virtual:08d35918703ec0bc2fb72b89407cc8430c63eb749ffe2bff641418cce72be4aab0afe750c610e41ee52e9bd2c8d7e721847c3791d36317060f0620130926424e#npm:28.1.1", {\
+          "packageLocation": "./.yarn/__virtual__/babel-preset-jest-virtual-081cd5e090/0/cache/babel-preset-jest-npm-28.1.1-05a1e38dd1-c581a81967.zip/node_modules/babel-preset-jest/",\
           "packageDependencies": [\
-            ["babel-preset-jest", "virtual:419439314f6ac7e6aeb104f74d9bd1fb754b552a3112b86e2807390519b56dbd6a88f32cff6e239d59a2670b389ec32d1afc7812be06ebe4cb1eeb9c2c58cf9e#npm:28.1.1"],\
+            ["babel-preset-jest", "virtual:08d35918703ec0bc2fb72b89407cc8430c63eb749ffe2bff641418cce72be4aab0afe750c610e41ee52e9bd2c8d7e721847c3791d36317060f0620130926424e#npm:28.1.1"],\
             ["@babel/core", "npm:7.18.5"],\
             ["@types/babel__core", "npm:7.1.19"],\
             ["babel-plugin-jest-hoist", "npm:28.1.1"],\
-            ["babel-preset-current-node-syntax", "virtual:a21946f32fecc6d2a4f39c804bc6851b8c98cf267db2bb0a25b0f443fe3cf1ff67012036ab014b3ec1309cc1f0a5678c35acb443e7d8c8a0d3c29071288e53d7#npm:1.0.1"]\
+            ["babel-preset-current-node-syntax", "virtual:081cd5e09087a97f33e6c2157b2f04624b270420cbd295805f8d59379d1fdc60eb969f610d8b11a66179e64b76fb08cb87d36cb6f67d17466aa40dc8ab796225#npm:1.0.1"]\
           ],\
           "packagePeers": [\
             "@babel/core",\
@@ -6173,14 +6270,14 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
           ],\
           "linkType": "SOFT"\
         }],\
-        ["virtual:16bfd8597041deb71e4581ea0755edd4dcd1b09b8ab14bfbbf5e4d5ca6b5d47ed7fbe2a25cdf57fcbb8e092c30b6beb93d2e7533f9e31c5dc62f7f0e487d1e4b#npm:4.0.0", {\
-          "packageLocation": "./.yarn/__virtual__/eslint-plugin-prettier-virtual-7e331f4408/0/cache/eslint-plugin-prettier-npm-4.0.0-e632552861-03d69177a3.zip/node_modules/eslint-plugin-prettier/",\
+        ["virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:4.0.0", {\
+          "packageLocation": "./.yarn/__virtual__/eslint-plugin-prettier-virtual-eaf1b54b4e/0/cache/eslint-plugin-prettier-npm-4.0.0-e632552861-03d69177a3.zip/node_modules/eslint-plugin-prettier/",\
           "packageDependencies": [\
-            ["eslint-plugin-prettier", "virtual:16bfd8597041deb71e4581ea0755edd4dcd1b09b8ab14bfbbf5e4d5ca6b5d47ed7fbe2a25cdf57fcbb8e092c30b6beb93d2e7533f9e31c5dc62f7f0e487d1e4b#npm:4.0.0"],\
+            ["eslint-plugin-prettier", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:4.0.0"],\
             ["@types/eslint", null],\
             ["@types/eslint-config-prettier", null],\
             ["@types/prettier", null],\
-            ["eslint", null],\
+            ["eslint", "npm:8.18.0"],\
             ["eslint-config-prettier", null],\
             ["prettier", null],\
             ["prettier-linter-helpers", "npm:1.0.0"]\
@@ -6195,14 +6292,14 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
           ],\
           "linkType": "HARD"\
         }],\
-        ["virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:4.0.0", {\
-          "packageLocation": "./.yarn/__virtual__/eslint-plugin-prettier-virtual-5c96b2a218/0/cache/eslint-plugin-prettier-npm-4.0.0-e632552861-03d69177a3.zip/node_modules/eslint-plugin-prettier/",\
+        ["virtual:16bfd8597041deb71e4581ea0755edd4dcd1b09b8ab14bfbbf5e4d5ca6b5d47ed7fbe2a25cdf57fcbb8e092c30b6beb93d2e7533f9e31c5dc62f7f0e487d1e4b#npm:4.0.0", {\
+          "packageLocation": "./.yarn/__virtual__/eslint-plugin-prettier-virtual-7e331f4408/0/cache/eslint-plugin-prettier-npm-4.0.0-e632552861-03d69177a3.zip/node_modules/eslint-plugin-prettier/",\
           "packageDependencies": [\
-            ["eslint-plugin-prettier", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:4.0.0"],\
+            ["eslint-plugin-prettier", "virtual:16bfd8597041deb71e4581ea0755edd4dcd1b09b8ab14bfbbf5e4d5ca6b5d47ed7fbe2a25cdf57fcbb8e092c30b6beb93d2e7533f9e31c5dc62f7f0e487d1e4b#npm:4.0.0"],\
             ["@types/eslint", null],\
             ["@types/eslint-config-prettier", null],\
             ["@types/prettier", null],\
-            ["eslint", "npm:8.18.0"],\
+            ["eslint", null],\
             ["eslint-config-prettier", null],\
             ["prettier", null],\
             ["prettier-linter-helpers", "npm:1.0.0"]\
@@ -7231,6 +7328,13 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
           ],\
           "linkType": "HARD"\
         }],\
+        ["npm:4.4.1", {\
+          "packageLocation": "./.yarn/cache/helmet-npm-4.4.1-286ac392ee-cfe385e185.zip/node_modules/helmet/",\
+          "packageDependencies": [\
+            ["helmet", "npm:4.4.1"]\
+          ],\
+          "linkType": "HARD"\
+        }],\
         ["npm:4.6.0", {\
           "packageLocation": "./.yarn/cache/helmet-npm-4.6.0-f244fd965c-139ad678d1.zip/node_modules/helmet/",\
           "packageDependencies": [\
@@ -8038,15 +8142,15 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
           ],\
           "linkType": "SOFT"\
         }],\
-        ["virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:28.1.1", {\
-          "packageLocation": "./.yarn/__virtual__/jest-virtual-6e20fd2eaa/0/cache/jest-npm-28.1.1-a4158efd82-398a143d9e.zip/node_modules/jest/",\
+        ["virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:28.1.1", {\
+          "packageLocation": "./.yarn/__virtual__/jest-virtual-c6df9164d8/0/cache/jest-npm-28.1.1-a4158efd82-398a143d9e.zip/node_modules/jest/",\
           "packageDependencies": [\
-            ["jest", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:28.1.1"],\
-            ["@jest/core", "virtual:6e20fd2eaaa940ccda315da6252a82baa2918f5ea3c40e2d7cb4d97f01b503d35a5076b4b63a33762fb1174e73a3313072cadf65e4a26d1b33660f964eda7880#npm:28.1.1"],\
+            ["jest", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:28.1.1"],\
+            ["@jest/core", "virtual:c6df9164d8eb22b719e1cd2b32bc24b485c129a3b28db796c6f3a6c22ad9410bc922a7f59cbbfe78d6492c9ee9f75de8ed7c2ec2f694d58e500097e2a2b74028#npm:28.1.1"],\
             ["@jest/types", "npm:28.1.1"],\
             ["@types/node-notifier", null],\
             ["import-local", "npm:3.1.0"],\
-            ["jest-cli", "virtual:6e20fd2eaaa940ccda315da6252a82baa2918f5ea3c40e2d7cb4d97f01b503d35a5076b4b63a33762fb1174e73a3313072cadf65e4a26d1b33660f964eda7880#npm:28.1.1"],\
+            ["jest-cli", "virtual:c6df9164d8eb22b719e1cd2b32bc24b485c129a3b28db796c6f3a6c22ad9410bc922a7f59cbbfe78d6492c9ee9f75de8ed7c2ec2f694d58e500097e2a2b74028#npm:28.1.1"],\
             ["node-notifier", null]\
           ],\
           "packagePeers": [\
@@ -8103,11 +8207,11 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
           ],\
           "linkType": "SOFT"\
         }],\
-        ["virtual:6e20fd2eaaa940ccda315da6252a82baa2918f5ea3c40e2d7cb4d97f01b503d35a5076b4b63a33762fb1174e73a3313072cadf65e4a26d1b33660f964eda7880#npm:28.1.1", {\
-          "packageLocation": "./.yarn/__virtual__/jest-cli-virtual-18fea92b00/0/cache/jest-cli-npm-28.1.1-7fb5826ae7-fce96f2f0c.zip/node_modules/jest-cli/",\
+        ["virtual:c6df9164d8eb22b719e1cd2b32bc24b485c129a3b28db796c6f3a6c22ad9410bc922a7f59cbbfe78d6492c9ee9f75de8ed7c2ec2f694d58e500097e2a2b74028#npm:28.1.1", {\
+          "packageLocation": "./.yarn/__virtual__/jest-cli-virtual-ffe7948780/0/cache/jest-cli-npm-28.1.1-7fb5826ae7-fce96f2f0c.zip/node_modules/jest-cli/",\
           "packageDependencies": [\
-            ["jest-cli", "virtual:6e20fd2eaaa940ccda315da6252a82baa2918f5ea3c40e2d7cb4d97f01b503d35a5076b4b63a33762fb1174e73a3313072cadf65e4a26d1b33660f964eda7880#npm:28.1.1"],\
-            ["@jest/core", "virtual:6e20fd2eaaa940ccda315da6252a82baa2918f5ea3c40e2d7cb4d97f01b503d35a5076b4b63a33762fb1174e73a3313072cadf65e4a26d1b33660f964eda7880#npm:28.1.1"],\
+            ["jest-cli", "virtual:c6df9164d8eb22b719e1cd2b32bc24b485c129a3b28db796c6f3a6c22ad9410bc922a7f59cbbfe78d6492c9ee9f75de8ed7c2ec2f694d58e500097e2a2b74028#npm:28.1.1"],\
+            ["@jest/core", "virtual:c6df9164d8eb22b719e1cd2b32bc24b485c129a3b28db796c6f3a6c22ad9410bc922a7f59cbbfe78d6492c9ee9f75de8ed7c2ec2f694d58e500097e2a2b74028#npm:28.1.1"],\
             ["@jest/test-result", "npm:28.1.1"],\
             ["@jest/types", "npm:28.1.1"],\
             ["@types/node-notifier", null],\
@@ -8115,7 +8219,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
             ["exit", "npm:0.1.2"],\
             ["graceful-fs", "npm:4.2.10"],\
             ["import-local", "npm:3.1.0"],\
-            ["jest-config", "virtual:18fea92b00a9a17809e3136cba934f07b76b6365d781cde6e4e8ad39518603c42b210c61bd96fbdefd3c18ea76e15da85ec1d250fdb153b485bb120f2884a87d#npm:28.1.1"],\
+            ["jest-config", "virtual:ffe7948780650e103d113b366bd00aa5fddf83f2645b1950ace5412f68838bb229734eeb6f43c5cf8e80750f2112707820eb53dac9ef0db7f3ad9a040066aa3b#npm:28.1.1"],\
             ["jest-util", "npm:28.1.1"],\
             ["jest-validate", "npm:28.1.1"],\
             ["node-notifier", null],\
@@ -8137,16 +8241,16 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
           ],\
           "linkType": "SOFT"\
         }],\
-        ["virtual:18fea92b00a9a17809e3136cba934f07b76b6365d781cde6e4e8ad39518603c42b210c61bd96fbdefd3c18ea76e15da85ec1d250fdb153b485bb120f2884a87d#npm:28.1.1", {\
-          "packageLocation": "./.yarn/__virtual__/jest-config-virtual-56145f3e40/0/cache/jest-config-npm-28.1.1-8c4e855059-8ce9f6b8f6.zip/node_modules/jest-config/",\
+        ["virtual:c650136efccbbb479cf2d5b8062777752af30a07867b5d5c0bde3cbc6a5c6f054afdfcc433e9ded24592d8fb0664ba4983e4f355a06678d9210b9c9e34f577b9#npm:28.1.1", {\
+          "packageLocation": "./.yarn/__virtual__/jest-config-virtual-32b5269fd2/0/cache/jest-config-npm-28.1.1-8c4e855059-8ce9f6b8f6.zip/node_modules/jest-config/",\
           "packageDependencies": [\
-            ["jest-config", "virtual:18fea92b00a9a17809e3136cba934f07b76b6365d781cde6e4e8ad39518603c42b210c61bd96fbdefd3c18ea76e15da85ec1d250fdb153b485bb120f2884a87d#npm:28.1.1"],\
+            ["jest-config", "virtual:c650136efccbbb479cf2d5b8062777752af30a07867b5d5c0bde3cbc6a5c6f054afdfcc433e9ded24592d8fb0664ba4983e4f355a06678d9210b9c9e34f577b9#npm:28.1.1"],\
             ["@babel/core", "npm:7.18.5"],\
             ["@jest/test-sequencer", "npm:28.1.1"],\
             ["@jest/types", "npm:28.1.1"],\
-            ["@types/node", null],\
+            ["@types/node", "npm:18.0.0"],\
             ["@types/ts-node", null],\
-            ["babel-jest", "virtual:805c813b6f046618cef5c7d6c026d202467ce267579e0c7a252be4f063439bc6f090ab5b924f50d7ae022b220d8e89e00ef15869e26244774ec68ef480e4e54d#npm:28.1.1"],\
+            ["babel-jest", "virtual:32b5269fd2e54a6fa7239e0777f3768bd33c5a094884dd5492350113f2bec8947944fe0156c37c044965c85742064fae363a3a7138decb195966e011bb7f7d4f#npm:28.1.1"],\
             ["chalk", "npm:4.1.2"],\
             ["ci-info", "npm:3.3.2"],\
             ["deepmerge", "npm:4.2.2"],\
@@ -8174,16 +8278,16 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
           ],\
           "linkType": "HARD"\
         }],\
-        ["virtual:4b45c3242ed36b84511b3946081e5d3b347e0463d6e39ebfdee2ad8392eb4bd7a5761a69e4fccf0d230c488b171720ddcf381e7c249fe8f4fcdf9d4afc493b87#npm:28.1.1", {\
-          "packageLocation": "./.yarn/__virtual__/jest-config-virtual-805c813b6f/0/cache/jest-config-npm-28.1.1-8c4e855059-8ce9f6b8f6.zip/node_modules/jest-config/",\
+        ["virtual:ffe7948780650e103d113b366bd00aa5fddf83f2645b1950ace5412f68838bb229734eeb6f43c5cf8e80750f2112707820eb53dac9ef0db7f3ad9a040066aa3b#npm:28.1.1", {\
+          "packageLocation": "./.yarn/__virtual__/jest-config-virtual-588b409452/0/cache/jest-config-npm-28.1.1-8c4e855059-8ce9f6b8f6.zip/node_modules/jest-config/",\
           "packageDependencies": [\
-            ["jest-config", "virtual:4b45c3242ed36b84511b3946081e5d3b347e0463d6e39ebfdee2ad8392eb4bd7a5761a69e4fccf0d230c488b171720ddcf381e7c249fe8f4fcdf9d4afc493b87#npm:28.1.1"],\
+            ["jest-config", "virtual:ffe7948780650e103d113b366bd00aa5fddf83f2645b1950ace5412f68838bb229734eeb6f43c5cf8e80750f2112707820eb53dac9ef0db7f3ad9a040066aa3b#npm:28.1.1"],\
             ["@babel/core", "npm:7.18.5"],\
             ["@jest/test-sequencer", "npm:28.1.1"],\
             ["@jest/types", "npm:28.1.1"],\
-            ["@types/node", "npm:18.0.0"],\
+            ["@types/node", null],\
             ["@types/ts-node", null],\
-            ["babel-jest", "virtual:805c813b6f046618cef5c7d6c026d202467ce267579e0c7a252be4f063439bc6f090ab5b924f50d7ae022b220d8e89e00ef15869e26244774ec68ef480e4e54d#npm:28.1.1"],\
+            ["babel-jest", "virtual:32b5269fd2e54a6fa7239e0777f3768bd33c5a094884dd5492350113f2bec8947944fe0156c37c044965c85742064fae363a3a7138decb195966e011bb7f7d4f#npm:28.1.1"],\
             ["chalk", "npm:4.1.2"],\
             ["ci-info", "npm:3.3.2"],\
             ["deepmerge", "npm:4.2.2"],\
@@ -11713,10 +11817,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
           ],\
           "linkType": "SOFT"\
         }],\
-        ["virtual:948d77a715c2182e00f2764d231af7afb02bf7f3164cf05b0248dc116c0a54912c77c570591f08b5b0dbb55984329e93f5f5704931076ba8f07fc46d4a69d072#npm:5.7.0", {\
-          "packageLocation": "./.yarn/__virtual__/sqs-consumer-virtual-b5ed97f086/0/cache/sqs-consumer-npm-5.7.0-09231a3791-d1eb00cbc5.zip/node_modules/sqs-consumer/",\
+        ["virtual:18a82f2f722cf47811304317f79c07fee6ecd8a887f3af2b42c72227f3b982ee05bf525aa8a7d2d20252ed23a1ed39ca99b430ed432b264a8bad79965c0cae5e#npm:5.7.0", {\
+          "packageLocation": "./.yarn/__virtual__/sqs-consumer-virtual-cbacaabf93/0/cache/sqs-consumer-npm-5.7.0-09231a3791-d1eb00cbc5.zip/node_modules/sqs-consumer/",\
           "packageDependencies": [\
-            ["sqs-consumer", "virtual:948d77a715c2182e00f2764d231af7afb02bf7f3164cf05b0248dc116c0a54912c77c570591f08b5b0dbb55984329e93f5f5704931076ba8f07fc46d4a69d072#npm:5.7.0"],\
+            ["sqs-consumer", "virtual:18a82f2f722cf47811304317f79c07fee6ecd8a887f3af2b42c72227f3b982ee05bf525aa8a7d2d20252ed23a1ed39ca99b430ed432b264a8bad79965c0cae5e#npm:5.7.0"],\
             ["@types/aws-sdk", null],\
             ["aws-sdk", "npm:2.1157.0"],\
             ["debug", "virtual:b86a9fb34323a98c6519528ed55faa0d9b44ca8879307c0b29aa384bde47ff59a7d0c9051b31246f14521dfb71ba3c5d6d0b35c29fffc17bf875aa6ad977d9e8#npm:4.3.4"]\
@@ -12233,21 +12337,21 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
           ],\
           "linkType": "SOFT"\
         }],\
-        ["virtual:16bfd8597041deb71e4581ea0755edd4dcd1b09b8ab14bfbbf5e4d5ca6b5d47ed7fbe2a25cdf57fcbb8e092c30b6beb93d2e7533f9e31c5dc62f7f0e487d1e4b#npm:28.0.5", {\
-          "packageLocation": "./.yarn/__virtual__/ts-jest-virtual-c9b832d80c/0/cache/ts-jest-npm-28.0.5-8c44d8b86f-53e05db5b7.zip/node_modules/ts-jest/",\
+        ["virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:28.0.5", {\
+          "packageLocation": "./.yarn/__virtual__/ts-jest-virtual-45e1f3673c/0/cache/ts-jest-npm-28.0.5-8c44d8b86f-53e05db5b7.zip/node_modules/ts-jest/",\
           "packageDependencies": [\
-            ["ts-jest", "virtual:16bfd8597041deb71e4581ea0755edd4dcd1b09b8ab14bfbbf5e4d5ca6b5d47ed7fbe2a25cdf57fcbb8e092c30b6beb93d2e7533f9e31c5dc62f7f0e487d1e4b#npm:28.0.5"],\
+            ["ts-jest", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:28.0.5"],\
             ["@babel/core", null],\
             ["@types/babel-jest", null],\
             ["@types/babel__core", null],\
             ["@types/esbuild", null],\
-            ["@types/jest", "npm:28.1.2"],\
+            ["@types/jest", "npm:28.1.3"],\
             ["@types/typescript", null],\
             ["babel-jest", null],\
             ["bs-logger", "npm:0.2.6"],\
             ["esbuild", null],\
             ["fast-json-stable-stringify", "npm:2.1.0"],\
-            ["jest", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:28.1.1"],\
+            ["jest", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:28.1.1"],\
             ["jest-util", "npm:28.1.1"],\
             ["json5", "npm:2.2.1"],\
             ["lodash.memoize", "npm:4.1.2"],\
@@ -12270,21 +12374,21 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
           ],\
           "linkType": "HARD"\
         }],\
-        ["virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:28.0.5", {\
-          "packageLocation": "./.yarn/__virtual__/ts-jest-virtual-21163ab03c/0/cache/ts-jest-npm-28.0.5-8c44d8b86f-53e05db5b7.zip/node_modules/ts-jest/",\
+        ["virtual:16bfd8597041deb71e4581ea0755edd4dcd1b09b8ab14bfbbf5e4d5ca6b5d47ed7fbe2a25cdf57fcbb8e092c30b6beb93d2e7533f9e31c5dc62f7f0e487d1e4b#npm:28.0.5", {\
+          "packageLocation": "./.yarn/__virtual__/ts-jest-virtual-c9b832d80c/0/cache/ts-jest-npm-28.0.5-8c44d8b86f-53e05db5b7.zip/node_modules/ts-jest/",\
           "packageDependencies": [\
-            ["ts-jest", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:28.0.5"],\
+            ["ts-jest", "virtual:16bfd8597041deb71e4581ea0755edd4dcd1b09b8ab14bfbbf5e4d5ca6b5d47ed7fbe2a25cdf57fcbb8e092c30b6beb93d2e7533f9e31c5dc62f7f0e487d1e4b#npm:28.0.5"],\
             ["@babel/core", null],\
             ["@types/babel-jest", null],\
             ["@types/babel__core", null],\
             ["@types/esbuild", null],\
-            ["@types/jest", "npm:28.1.3"],\
+            ["@types/jest", "npm:28.1.2"],\
             ["@types/typescript", null],\
             ["babel-jest", null],\
             ["bs-logger", "npm:0.2.6"],\
             ["esbuild", null],\
             ["fast-json-stable-stringify", "npm:2.1.0"],\
-            ["jest", "virtual:31b5a94a105c89c9294c3d524a7f8929fe63ee5a2efadf21951ca4c0cfd2ecf02e8f4ef5a066bbda091f1e3a56e57c6749069a080618c96b22e51131a330fc4a#npm:28.1.1"],\
+            ["jest", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:28.1.1"],\
             ["jest-util", "npm:28.1.1"],\
             ["json5", "npm:2.2.1"],\
             ["lodash.memoize", "npm:4.1.2"],\
@@ -12463,10 +12567,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
           ],\
           "linkType": "HARD"\
         }],\
-        ["virtual:e64d2841693653abb2dee666d19406912f5e913a8081a709c081d9877d2f39987ff853b7cd736901a2df59af98328f7249f3db0da01abf060cf1d858d4d4e43b#npm:3.21.0", {\
-          "packageLocation": "./.yarn/__virtual__/tsutils-virtual-cd74663c37/0/cache/tsutils-npm-3.21.0-347e6636c5-1843f4c1b2.zip/node_modules/tsutils/",\
+        ["virtual:d2330b8caf9d1ed905d5d037b08afe5d2dcb2220d795f2d96b3dd514824e8aefae721c334dbd0cd8d53c639ff2c9ad893d3d83b1092b2db05593aa9dc8e59994#npm:3.21.0", {\
+          "packageLocation": "./.yarn/__virtual__/tsutils-virtual-f8ae8a51d8/0/cache/tsutils-npm-3.21.0-347e6636c5-1843f4c1b2.zip/node_modules/tsutils/",\
           "packageDependencies": [\
-            ["tsutils", "virtual:e64d2841693653abb2dee666d19406912f5e913a8081a709c081d9877d2f39987ff853b7cd736901a2df59af98328f7249f3db0da01abf060cf1d858d4d4e43b#npm:3.21.0"],\
+            ["tsutils", "virtual:d2330b8caf9d1ed905d5d037b08afe5d2dcb2220d795f2d96b3dd514824e8aefae721c334dbd0cd8d53c639ff2c9ad893d3d83b1092b2db05593aa9dc8e59994#npm:3.21.0"],\
             ["@types/typescript", null],\
             ["tslib", "npm:1.14.1"],\
             ["typescript", null]\

BIN
.yarn/cache/@standardnotes-auth-npm-3.19.2-5289525e60-2e4b37b303.zip


BIN
.yarn/cache/@standardnotes-domain-events-infra-npm-1.4.127-18a82f2f72-54e37c296f.zip


BIN
.yarn/cache/@standardnotes-domain-events-npm-2.29.0-13bec3d9a7-1b68999e2a.zip


BIN
.yarn/cache/aws-sdk-npm-2.1160.0-1a3db600b7-b95647d4de.zip


BIN
.yarn/cache/helmet-npm-4.4.1-286ac392ee-cfe385e185.zip


+ 3 - 0
package.json

@@ -16,6 +16,7 @@
     "lint:scheduler": "yarn workspace @standardnotes/scheduler-server lint",
     "lint:syncing-server": "yarn workspace @standardnotes/syncing-server lint",
     "lint:files": "yarn workspace @standardnotes/files-server lint",
+    "lint:api-gateway": "yarn workspace @standardnotes/api-gateway lint",
     "test": "yarn workspaces foreach -p -j 10 --verbose run test",
     "test:auth": "yarn workspace @standardnotes/auth-server test",
     "test:scheduler": "yarn workspace @standardnotes/scheduler-server test",
@@ -28,6 +29,7 @@
     "build:scheduler": "yarn workspace @standardnotes/scheduler-server build",
     "build:syncing-server": "yarn workspace @standardnotes/syncing-server build",
     "build:files": "yarn workspace @standardnotes/files-server build",
+    "build:api-gateway": "yarn workspace @standardnotes/api-gateway build",
     "start:auth": "yarn workspace @standardnotes/auth-server start",
     "start:auth-worker": "yarn workspace @standardnotes/auth-server worker",
     "start:scheduler": "yarn workspace @standardnotes/scheduler-server worker",
@@ -35,6 +37,7 @@
     "start:syncing-server-worker": "yarn workspace @standardnotes/syncing-server worker",
     "start:files": "yarn workspace @standardnotes/files-server start",
     "start:files-worker": "yarn workspace @standardnotes/files-server worker",
+    "start:api-gateway": "yarn workspace @standardnotes/api-gateway start",
     "release:beta": "lerna version --conventional-prerelease --conventional-commits --yes -m \"chore(release): publish\""
   },
   "devDependencies": {

+ 33 - 0
packages/api-gateway/.env.sample

@@ -0,0 +1,33 @@
+LOG_LEVEL=debug
+NODE_ENV=development
+VERSION=development
+
+PORT=3000
+
+SYNCING_SERVER_JS_URL=http://syncing_server_js:3000
+AUTH_SERVER_URL=http://auth:3000
+PAYMENTS_SERVER_URL=http://payments:3000
+FILES_SERVER_URL=http://files:3000
+
+HTTP_CALL_TIMEOUT=60000
+
+AUTH_JWT_SECRET=auth_jwt_secret
+
+# (Optional) New Relic Setup
+NEW_RELIC_ENABLED=false
+NEW_RELIC_APP_NAME=API Gateway
+NEW_RELIC_LICENSE_KEY=
+NEW_RELIC_NO_CONFIG_FILE=true
+NEW_RELIC_DISTRIBUTED_TRACING_ENABLED=false
+NEW_RELIC_LOG_ENABLED=false
+NEW_RELIC_LOG_LEVEL=info
+
+REDIS_URL=redis://cache
+REDIS_EVENTS_CHANNEL=events
+
+# (Optional) SNS Setup
+SNS_TOPIC_ARN=
+SNS_AWS_REGION=
+
+# (Optional) Caching Cross Service Tokens
+CROSS_SERVICE_TOKEN_CACHE_TTL=

+ 2 - 0
packages/api-gateway/.eslintignore

@@ -0,0 +1,2 @@
+dist
+test-setup.ts

+ 6 - 0
packages/api-gateway/.eslintrc

@@ -0,0 +1,6 @@
+{
+  "extends": "../../.eslintrc",
+  "parserOptions": {
+    "project": "./linter.tsconfig.json"
+  }
+}

+ 25 - 0
packages/api-gateway/Dockerfile

@@ -0,0 +1,25 @@
+FROM node:16.15.1-alpine AS builder
+
+# Install dependencies for building native libraries
+RUN apk add --update git openssh-client python3 alpine-sdk
+
+WORKDIR /workspace
+
+# docker-build plugin copies everything needed for `yarn install` to `manifests` folder.
+COPY manifests ./
+
+RUN yarn install --immutable
+
+FROM node:16.15.1-alpine
+
+WORKDIR /workspace
+
+# Copy the installed dependencies from the previous stage.
+COPY --from=builder /workspace ./
+
+# docker-build plugin runs `yarn pack` in all workspace dependencies and copies them to `packs` folder.
+COPY packs ./
+
+ENTRYPOINT [ "/workspace/packages/api-gateway/docker/entrypoint.sh" ]
+
+CMD [ "start-web" ]

+ 75 - 0
packages/api-gateway/bin/report.ts

@@ -0,0 +1,75 @@
+import 'reflect-metadata'
+
+import 'newrelic'
+
+import { Logger } from 'winston'
+
+import { ContainerConfigLoader } from '../src/Bootstrap/Container'
+import TYPES from '../src/Bootstrap/Types'
+import { Env } from '../src/Bootstrap/Env'
+import { DomainEventPublisherInterface, DailyAnalyticsReportGeneratedEvent } from '@standardnotes/domain-events'
+import { AnalyticsActivity, AnalyticsStoreInterface, Period, StatisticsStoreInterface } from '@standardnotes/analytics'
+
+const requestReport = async (
+  analyticsStore: AnalyticsStoreInterface,
+  statisticsStore: StatisticsStoreInterface,
+  domainEventPublisher: DomainEventPublisherInterface,
+): Promise<void> => {
+  const event: DailyAnalyticsReportGeneratedEvent = {
+    type: 'DAILY_ANALYTICS_REPORT_GENERATED',
+    createdAt: new Date(),
+    meta: {
+      correlation: {
+        userIdentifier: '',
+        userIdentifierType: 'uuid',
+      },
+    },
+    payload: {
+      applicationStatistics: await statisticsStore.getYesterdayApplicationUsage(),
+      snjsStatistics: await statisticsStore.getYesterdaySNJSUsage(),
+      outOfSyncIncidents: await statisticsStore.getYesterdayOutOfSyncIncidents(),
+      activityStatistics: [
+        {
+          name: AnalyticsActivity.EditingItems,
+          retention: await analyticsStore.calculateActivityRetention(
+            AnalyticsActivity.EditingItems,
+            Period.DayBeforeYesterday,
+            Period.Yesterday,
+          ),
+          totalCount: await analyticsStore.calculateActivityTotalCount(
+            AnalyticsActivity.EditingItems,
+            Period.Yesterday,
+          ),
+        },
+      ],
+    },
+  }
+
+  await domainEventPublisher.publish(event)
+}
+
+const container = new ContainerConfigLoader()
+void container.load().then((container) => {
+  const env: Env = new Env()
+  env.load()
+
+  const logger: Logger = container.get(TYPES.Logger)
+
+  logger.info('Starting usage report generation...')
+
+  const analyticsStore: AnalyticsStoreInterface = container.get(TYPES.AnalyticsStore)
+  const statisticsStore: StatisticsStoreInterface = container.get(TYPES.StatisticsStore)
+  const domainEventPublisher: DomainEventPublisherInterface = container.get(TYPES.DomainEventPublisher)
+
+  Promise.resolve(requestReport(analyticsStore, statisticsStore, domainEventPublisher))
+    .then(() => {
+      logger.info('Usage report generation complete')
+
+      process.exit(0)
+    })
+    .catch((error) => {
+      logger.error(`Could not finish usage report generation: ${error.message}`)
+
+      process.exit(1)
+    })
+})

+ 113 - 0
packages/api-gateway/bin/server.ts

@@ -0,0 +1,113 @@
+import 'reflect-metadata'
+
+import 'newrelic'
+
+import * as Sentry from '@sentry/node'
+
+import '../src/Controller/LegacyController'
+import '../src/Controller/HealthCheckController'
+
+import '../src/Controller/v1/SessionsController'
+import '../src/Controller/v1/UsersController'
+import '../src/Controller/v1/ActionsController'
+import '../src/Controller/v1/InvoicesController'
+import '../src/Controller/v1/RevisionsController'
+import '../src/Controller/v1/ItemsController'
+import '../src/Controller/v1/PaymentsController'
+import '../src/Controller/v1/WebSocketsController'
+import '../src/Controller/v1/TokensController'
+import '../src/Controller/v1/OfflineController'
+import '../src/Controller/v1/FilesController'
+import '../src/Controller/v1/SubscriptionInvitesController'
+
+import '../src/Controller/v2/PaymentsControllerV2'
+import '../src/Controller/v2/ActionsControllerV2'
+
+import * as helmet from 'helmet'
+import * as cors from 'cors'
+import { text, json, Request, Response, NextFunction, RequestHandler, ErrorRequestHandler } from 'express'
+import * as winston from 'winston'
+
+import { InversifyExpressServer } from 'inversify-express-utils'
+import { ContainerConfigLoader } from '../src/Bootstrap/Container'
+import TYPES from '../src/Bootstrap/Types'
+import { Env } from '../src/Bootstrap/Env'
+
+const container = new ContainerConfigLoader()
+void container.load().then((container) => {
+  const env: Env = new Env()
+  env.load()
+
+  const server = new InversifyExpressServer(container)
+
+  server.setConfig((app) => {
+    app.use((_request: Request, response: Response, next: NextFunction) => {
+      response.setHeader('X-API-Gateway-Version', container.get(TYPES.VERSION))
+      next()
+    })
+    /* eslint-disable */
+    app.use(helmet({
+      contentSecurityPolicy: {
+        directives: {
+          defaultSrc: ["https: 'self'"],
+          baseUri: ["'self'"],
+          childSrc: ["*", "blob:"],
+          connectSrc: ["*"],
+          fontSrc: ["*", "'self'"],
+          formAction: ["'self'"],
+          frameAncestors: ["*", "*.standardnotes.org", "*.standardnotes.com"],
+          frameSrc: ["*", "blob:"],
+          imgSrc: ["'self'", "*", "data:"],
+          manifestSrc: ["'self'"],
+          mediaSrc: ["'self'"],
+          objectSrc: ["'self'"],
+          scriptSrc: ["'self'"],
+          styleSrc: ["'self'"]
+        }
+      }
+    }))
+    /* eslint-enable */
+    app.use(json({ limit: '50mb' }))
+    app.use(
+      text({
+        type: ['text/plain', 'application/x-www-form-urlencoded', 'application/x-www-form-urlencoded; charset=utf-8'],
+      }),
+    )
+    app.use(cors())
+
+    if (env.get('SENTRY_DSN', true)) {
+      Sentry.init({
+        dsn: env.get('SENTRY_DSN'),
+        integrations: [new Sentry.Integrations.Http({ tracing: false, breadcrumbs: true })],
+        tracesSampleRate: 0,
+      })
+
+      app.use(Sentry.Handlers.requestHandler() as RequestHandler)
+    }
+  })
+
+  const logger: winston.Logger = container.get(TYPES.Logger)
+
+  server.setErrorConfig((app) => {
+    if (env.get('SENTRY_DSN', true)) {
+      app.use(Sentry.Handlers.errorHandler() as ErrorRequestHandler)
+    }
+
+    app.use((error: Record<string, unknown>, _request: Request, response: Response, _next: NextFunction) => {
+      logger.error(error.stack)
+
+      response.status(500).send({
+        error: {
+          message:
+            "Unfortunately, we couldn't handle your request. Please try again or contact our support if the error persists.",
+        },
+      })
+    })
+  })
+
+  const serverInstance = server.build()
+
+  serverInstance.listen(env.get('PORT'))
+
+  logger.info(`Server started on port ${process.env.PORT}`)
+})

+ 29 - 0
packages/api-gateway/docker/entrypoint.sh

@@ -0,0 +1,29 @@
+#!/bin/sh
+set -e
+
+COMMAND=$1 && shift 1
+
+case "$COMMAND" in
+  'start-local' )
+    echo "Building the project..."
+    yarn workspace @standardnotes/api-gateway build
+    echo "Starting Web..."
+    yarn workspace @standardnotes/api-gateway start
+    ;;
+
+  'start-web' )
+    echo "Starting Web..."
+    yarn workspace @standardnotes/api-gateway start
+    ;;
+
+  'report' )
+    echo "Starting Usage Report Generation..."
+    yarn workspace @standardnotes/api-gateway report
+    ;;
+
+   * )
+    echo "Unknown command"
+    ;;
+esac
+
+exec "$@"

+ 18 - 0
packages/api-gateway/jest.config.js

@@ -0,0 +1,18 @@
+// eslint-disable-next-line @typescript-eslint/no-var-requires
+const base = require('../../jest.config');
+
+module.exports = {
+  ...base,
+  globals: {
+    'ts-jest': {
+      tsconfig: 'tsconfig.json',
+    },
+  },
+  coveragePathIgnorePatterns: [
+    '/Bootstrap/',
+    'HealthCheckController'
+  ],
+  setupFilesAfterEnv: [
+    './test-setup.ts'
+  ]
+};

+ 4 - 0
packages/api-gateway/linter.tsconfig.json

@@ -0,0 +1,4 @@
+{
+  "extends": "./tsconfig.json",
+  "exclude": ["dist", "test-setup.ts"]
+}

+ 60 - 0
packages/api-gateway/package.json

@@ -0,0 +1,60 @@
+{
+  "name": "@standardnotes/api-gateway",
+  "version": "1.0.0",
+  "engines": {
+    "node": ">=16.0.0 <17.0.0"
+  },
+  "description": "API Gateway For Standard Notes Services",
+  "main": "dist/src/index.js",
+  "typings": "dist/src/index.d.ts",
+  "repository": "git@github.com:standardnotes/api-gateway.git",
+  "author": "Karol Sójko <karolsojko@standardnotes.com>",
+  "license": "AGPL-3.0-or-later",
+  "scripts": {
+    "clean": "rm -fr dist",
+    "prebuild": "yarn clean",
+    "build": "tsc --rootDir ./",
+    "lint": "eslint . --ext .ts",
+    "start": "yarn node dist/bin/server.js",
+    "report": "yarn node dist/bin/report.js"
+  },
+  "dependencies": {
+    "@newrelic/native-metrics": "7.0.2",
+    "@newrelic/winston-enricher": "^2.1.0",
+    "@sentry/node": "^6.16.1",
+    "@standardnotes/analytics": "^1.4.0",
+    "@standardnotes/auth": "3.19.2",
+    "@standardnotes/domain-events": "2.29.0",
+    "@standardnotes/domain-events-infra": "1.4.127",
+    "@standardnotes/time": "^1.7.0",
+    "aws-sdk": "^2.1160.0",
+    "axios": "0.24.0",
+    "cors": "2.8.5",
+    "dotenv": "8.2.0",
+    "express": "4.17.1",
+    "helmet": "4.4.1",
+    "inversify": "^6.0.1",
+    "inversify-express-utils": "^6.4.3",
+    "ioredis": "^5.0.6",
+    "jsonwebtoken": "8.5.1",
+    "newrelic": "8.6.0",
+    "prettyjson": "1.2.1",
+    "reflect-metadata": "0.1.13",
+    "winston": "3.3.3"
+  },
+  "devDependencies": {
+    "@types/cors": "^2.8.9",
+    "@types/express": "^4.17.11",
+    "@types/ioredis": "^4.28.10",
+    "@types/jest": "^28.1.3",
+    "@types/jsonwebtoken": "^8.5.0",
+    "@types/newrelic": "^7.0.1",
+    "@types/prettyjson": "^0.0.29",
+    "@typescript-eslint/eslint-plugin": "^5.29.0",
+    "eslint": "^8.14.0",
+    "eslint-plugin-prettier": "^4.0.0",
+    "jest": "^28.1.1",
+    "nodemon": "^2.0.16",
+    "ts-jest": "^28.0.1"
+  }
+}

+ 117 - 0
packages/api-gateway/src/Bootstrap/Container.ts

@@ -0,0 +1,117 @@
+import * as winston from 'winston'
+import axios, { AxiosInstance } from 'axios'
+import Redis from 'ioredis'
+import { Container } from 'inversify'
+import * as AWS from 'aws-sdk'
+import {
+  AnalyticsStoreInterface,
+  PeriodKeyGenerator,
+  RedisAnalyticsStore,
+  RedisStatisticsStore,
+  StatisticsStoreInterface,
+} from '@standardnotes/analytics'
+import { RedisDomainEventPublisher, SNSDomainEventPublisher } from '@standardnotes/domain-events-infra'
+import { Timer, TimerInterface } from '@standardnotes/time'
+
+import { Env } from './Env'
+import TYPES from './Types'
+import { AuthMiddleware } from '../Controller/AuthMiddleware'
+import { HttpServiceInterface } from '../Service/Http/HttpServiceInterface'
+import { HttpService } from '../Service/Http/HttpService'
+import { SubscriptionTokenAuthMiddleware } from '../Controller/SubscriptionTokenAuthMiddleware'
+import { StatisticsMiddleware } from '../Controller/StatisticsMiddleware'
+import { CrossServiceTokenCacheInterface } from '../Service/Cache/CrossServiceTokenCacheInterface'
+import { RedisCrossServiceTokenCache } from '../Infra/Redis/RedisCrossServiceTokenCache'
+
+// eslint-disable-next-line @typescript-eslint/no-var-requires
+const newrelicWinstonEnricher = require('@newrelic/winston-enricher')
+
+export class ContainerConfigLoader {
+  async load(): Promise<Container> {
+    const env: Env = new Env()
+    env.load()
+
+    const container = new Container()
+
+    const winstonFormatters = [winston.format.splat(), winston.format.json()]
+    if (env.get('NEW_RELIC_ENABLED', true) === 'true') {
+      winstonFormatters.push(newrelicWinstonEnricher())
+    }
+
+    const logger = winston.createLogger({
+      level: env.get('LOG_LEVEL') || 'info',
+      format: winston.format.combine(...winstonFormatters),
+      transports: [new winston.transports.Console({ level: env.get('LOG_LEVEL') || 'info' })],
+    })
+    container.bind<winston.Logger>(TYPES.Logger).toConstantValue(logger)
+
+    const redisUrl = env.get('REDIS_URL')
+    const isRedisInClusterMode = redisUrl.indexOf(',') > 0
+    let redis
+    if (isRedisInClusterMode) {
+      redis = new Redis.Cluster(redisUrl.split(','))
+    } else {
+      redis = new Redis(redisUrl)
+    }
+    container.bind(TYPES.Redis).toConstantValue(redis)
+
+    if (env.get('SNS_AWS_REGION', true)) {
+      container.bind<AWS.SNS>(TYPES.SNS).toConstantValue(
+        new AWS.SNS({
+          apiVersion: 'latest',
+          region: env.get('SNS_AWS_REGION', true),
+        }),
+      )
+    }
+
+    container.bind<AxiosInstance>(TYPES.HTTPClient).toConstantValue(axios.create())
+
+    // env vars
+    container.bind(TYPES.SYNCING_SERVER_JS_URL).toConstantValue(env.get('SYNCING_SERVER_JS_URL'))
+    container.bind(TYPES.AUTH_SERVER_URL).toConstantValue(env.get('AUTH_SERVER_URL'))
+    container.bind(TYPES.PAYMENTS_SERVER_URL).toConstantValue(env.get('PAYMENTS_SERVER_URL', true))
+    container.bind(TYPES.FILES_SERVER_URL).toConstantValue(env.get('FILES_SERVER_URL', true))
+    container.bind(TYPES.AUTH_JWT_SECRET).toConstantValue(env.get('AUTH_JWT_SECRET'))
+    container
+      .bind(TYPES.HTTP_CALL_TIMEOUT)
+      .toConstantValue(env.get('HTTP_CALL_TIMEOUT', true) ? +env.get('HTTP_CALL_TIMEOUT', true) : 60_000)
+    container.bind(TYPES.VERSION).toConstantValue(env.get('VERSION'))
+    container.bind(TYPES.SNS_TOPIC_ARN).toConstantValue(env.get('SNS_TOPIC_ARN', true))
+    container.bind(TYPES.SNS_AWS_REGION).toConstantValue(env.get('SNS_AWS_REGION', true))
+    container.bind(TYPES.REDIS_EVENTS_CHANNEL).toConstantValue(env.get('REDIS_EVENTS_CHANNEL'))
+    container.bind(TYPES.CROSS_SERVICE_TOKEN_CACHE_TTL).toConstantValue(+env.get('CROSS_SERVICE_TOKEN_CACHE_TTL', true))
+
+    // Middleware
+    container.bind<AuthMiddleware>(TYPES.AuthMiddleware).to(AuthMiddleware)
+    container
+      .bind<SubscriptionTokenAuthMiddleware>(TYPES.SubscriptionTokenAuthMiddleware)
+      .to(SubscriptionTokenAuthMiddleware)
+    container.bind<StatisticsMiddleware>(TYPES.StatisticsMiddleware).to(StatisticsMiddleware)
+
+    // Services
+    container.bind<HttpServiceInterface>(TYPES.HTTPService).to(HttpService)
+    const periodKeyGenerator = new PeriodKeyGenerator()
+    container
+      .bind<AnalyticsStoreInterface>(TYPES.AnalyticsStore)
+      .toConstantValue(new RedisAnalyticsStore(periodKeyGenerator, container.get(TYPES.Redis)))
+    container
+      .bind<StatisticsStoreInterface>(TYPES.StatisticsStore)
+      .toConstantValue(new RedisStatisticsStore(periodKeyGenerator, container.get(TYPES.Redis)))
+    container.bind<CrossServiceTokenCacheInterface>(TYPES.CrossServiceTokenCache).to(RedisCrossServiceTokenCache)
+    container.bind<TimerInterface>(TYPES.Timer).toConstantValue(new Timer())
+
+    if (env.get('SNS_TOPIC_ARN', true)) {
+      container
+        .bind<SNSDomainEventPublisher>(TYPES.DomainEventPublisher)
+        .toConstantValue(new SNSDomainEventPublisher(container.get(TYPES.SNS), container.get(TYPES.SNS_TOPIC_ARN)))
+    } else {
+      container
+        .bind<RedisDomainEventPublisher>(TYPES.DomainEventPublisher)
+        .toConstantValue(
+          new RedisDomainEventPublisher(container.get(TYPES.Redis), container.get(TYPES.REDIS_EVENTS_CHANNEL)),
+        )
+    }
+
+    return container
+  }
+}

+ 24 - 0
packages/api-gateway/src/Bootstrap/Env.ts

@@ -0,0 +1,24 @@
+import { config, DotenvParseOutput } from 'dotenv'
+import { injectable } from 'inversify'
+
+@injectable()
+export class Env {
+  private env?: DotenvParseOutput
+
+  public load(): void {
+    const output = config()
+    this.env = <DotenvParseOutput>output.parsed
+  }
+
+  public get(key: string, optional = false): string {
+    if (!this.env) {
+      this.load()
+    }
+
+    if (!process.env[key] && !optional) {
+      throw new Error(`Environment variable ${key} not set`)
+    }
+
+    return <string>process.env[key]
+  }
+}

+ 31 - 0
packages/api-gateway/src/Bootstrap/Types.ts

@@ -0,0 +1,31 @@
+const TYPES = {
+  Logger: Symbol.for('Logger'),
+  Redis: Symbol.for('Redis'),
+  HTTPClient: Symbol.for('HTTPClient'),
+  SNS: Symbol.for('SNS'),
+  // env vars
+  SYNCING_SERVER_JS_URL: Symbol.for('SYNCING_SERVER_JS_URL'),
+  AUTH_SERVER_URL: Symbol.for('AUTH_SERVER_URL'),
+  PAYMENTS_SERVER_URL: Symbol.for('PAYMENTS_SERVER_URL'),
+  FILES_SERVER_URL: Symbol.for('FILES_SERVER_URL'),
+  AUTH_JWT_SECRET: Symbol.for('AUTH_JWT_SECRET'),
+  HTTP_CALL_TIMEOUT: Symbol.for('HTTP_CALL_TIMEOUT'),
+  VERSION: Symbol.for('VERSION'),
+  SNS_TOPIC_ARN: Symbol.for('SNS_TOPIC_ARN'),
+  SNS_AWS_REGION: Symbol.for('SNS_AWS_REGION'),
+  REDIS_EVENTS_CHANNEL: Symbol.for('REDIS_EVENTS_CHANNEL'),
+  CROSS_SERVICE_TOKEN_CACHE_TTL: Symbol.for('CROSS_SERVICE_TOKEN_CACHE_TTL'),
+  // Middleware
+  StatisticsMiddleware: Symbol.for('StatisticsMiddleware'),
+  AuthMiddleware: Symbol.for('AuthMiddleware'),
+  SubscriptionTokenAuthMiddleware: Symbol.for('SubscriptionTokenAuthMiddleware'),
+  // Services
+  HTTPService: Symbol.for('HTTPService'),
+  CrossServiceTokenCache: Symbol.for('CrossServiceTokenCache'),
+  AnalyticsStore: Symbol.for('AnalyticsStore'),
+  StatisticsStore: Symbol.for('StatisticsStore'),
+  DomainEventPublisher: Symbol.for('DomainEventPublisher'),
+  Timer: Symbol.for('Timer'),
+}
+
+export default TYPES

+ 124 - 0
packages/api-gateway/src/Controller/AuthMiddleware.ts

@@ -0,0 +1,124 @@
+import { CrossServiceTokenData } from '@standardnotes/auth'
+import { TimerInterface } from '@standardnotes/time'
+import { NextFunction, Request, Response } from 'express'
+import { inject, injectable } from 'inversify'
+import { BaseMiddleware } from 'inversify-express-utils'
+import { verify } from 'jsonwebtoken'
+import { AxiosError, AxiosInstance } from 'axios'
+import { Logger } from 'winston'
+
+import TYPES from '../Bootstrap/Types'
+import { CrossServiceTokenCacheInterface } from '../Service/Cache/CrossServiceTokenCacheInterface'
+
+@injectable()
+export class AuthMiddleware extends BaseMiddleware {
+  constructor(
+    @inject(TYPES.HTTPClient) private httpClient: AxiosInstance,
+    @inject(TYPES.AUTH_SERVER_URL) private authServerUrl: string,
+    @inject(TYPES.AUTH_JWT_SECRET) private jwtSecret: string,
+    @inject(TYPES.CROSS_SERVICE_TOKEN_CACHE_TTL) private crossServiceTokenCacheTTL: number,
+    @inject(TYPES.CrossServiceTokenCache) private crossServiceTokenCache: CrossServiceTokenCacheInterface,
+    @inject(TYPES.Timer) private timer: TimerInterface,
+    @inject(TYPES.Logger) private logger: Logger,
+  ) {
+    super()
+  }
+
+  async handler(request: Request, response: Response, next: NextFunction): Promise<void> {
+    const authHeaderValue = request.headers.authorization as string
+
+    if (!authHeaderValue) {
+      response.status(401).send({
+        error: {
+          tag: 'invalid-auth',
+          message: 'Invalid login credentials.',
+        },
+      })
+
+      return
+    }
+
+    try {
+      let crossServiceTokenFetchedFromCache = true
+      let crossServiceToken = null
+      if (this.crossServiceTokenCacheTTL) {
+        crossServiceToken = await this.crossServiceTokenCache.get(authHeaderValue)
+      }
+
+      if (crossServiceToken === null) {
+        const authResponse = await this.httpClient.request({
+          method: 'POST',
+          headers: {
+            Authorization: authHeaderValue,
+            Accept: 'application/json',
+          },
+          validateStatus: (status: number) => {
+            return status >= 200 && status < 500
+          },
+          url: `${this.authServerUrl}/sessions/validate`,
+        })
+
+        if (authResponse.status > 200) {
+          response.setHeader('content-type', authResponse.headers['content-type'])
+          response.status(authResponse.status).send(authResponse.data)
+
+          return
+        }
+
+        crossServiceToken = authResponse.data.authToken
+        crossServiceTokenFetchedFromCache = false
+      }
+
+      response.locals.authToken = crossServiceToken
+
+      const decodedToken = <CrossServiceTokenData>verify(crossServiceToken, this.jwtSecret, { algorithms: ['HS256'] })
+
+      if (this.crossServiceTokenCacheTTL && !crossServiceTokenFetchedFromCache) {
+        await this.crossServiceTokenCache.set({
+          authorizationHeaderValue: authHeaderValue,
+          encodedCrossServiceToken: crossServiceToken,
+          expiresAtInSeconds: this.getCrossServiceTokenCacheExpireTimestamp(decodedToken),
+          userUuid: decodedToken.user.uuid,
+        })
+      }
+
+      response.locals.userUuid = decodedToken.user.uuid
+      response.locals.roles = decodedToken.roles
+    } catch (error) {
+      const errorMessage = (error as AxiosError).isAxiosError
+        ? JSON.stringify((error as AxiosError).response?.data)
+        : (error as Error).message
+
+      this.logger.error(
+        `Could not pass the request to ${this.authServerUrl}/sessions/validate on underlying service: ${errorMessage}`,
+      )
+
+      this.logger.debug('Response error: %O', (error as AxiosError).response ?? error)
+
+      if ((error as AxiosError).response?.headers['content-type']) {
+        response.setHeader('content-type', (error as AxiosError).response?.headers['content-type'] as string)
+      }
+
+      const errorCode = (error as AxiosError).isAxiosError ? +((error as AxiosError).code as string) : 500
+
+      response.status(errorCode).send(errorMessage)
+
+      return
+    }
+
+    return next()
+  }
+
+  private getCrossServiceTokenCacheExpireTimestamp(token: CrossServiceTokenData): number {
+    const crossServiceTokenDefaultCacheExpiration = this.timer.getTimestampInSeconds() + this.crossServiceTokenCacheTTL
+
+    if (token.session === undefined) {
+      return crossServiceTokenDefaultCacheExpiration
+    }
+
+    const sessionAccessExpiration = this.timer.convertStringDateToSeconds(token.session.access_expiration)
+    const sessionRefreshExpiration = this.timer.convertStringDateToSeconds(token.session.refresh_expiration)
+
+    return Math.min(crossServiceTokenDefaultCacheExpiration, sessionAccessExpiration, sessionRefreshExpiration)
+  }
+}

+ 9 - 0
packages/api-gateway/src/Controller/HealthCheckController.ts

@@ -0,0 +1,9 @@
+import { controller, httpGet } from 'inversify-express-utils'
+
+@controller('/healthcheck')
+export class HealthCheckController {
+  @httpGet('/')
+  public async get(): Promise<string> {
+    return 'OK'
+  }
+}

+ 124 - 0
packages/api-gateway/src/Controller/LegacyController.ts

@@ -0,0 +1,124 @@
+import { Request, Response } from 'express'
+import { inject } from 'inversify'
+import { controller, all, BaseHttpController, httpPost, httpGet, results, httpDelete } from 'inversify-express-utils'
+import TYPES from '../Bootstrap/Types'
+import { HttpServiceInterface } from '../Service/Http/HttpServiceInterface'
+
+@controller('', TYPES.StatisticsMiddleware)
+export class LegacyController extends BaseHttpController {
+  private AUTH_ROUTES: Map<string, string>
+  private PARAMETRIZED_AUTH_ROUTES: Map<string, string>
+
+  constructor(@inject(TYPES.HTTPService) private httpService: HttpServiceInterface) {
+    super()
+
+    this.AUTH_ROUTES = new Map([
+      ['POST:/auth', 'POST:auth'],
+      ['POST:/auth/sign_out', 'POST:auth/sign_out'],
+      ['POST:/auth/change_pw', 'PUT:/users/legacy-endpoint-user/attributes/credentials'],
+      ['GET:/sessions', 'GET:sessions'],
+      ['DELETE:/session', 'DELETE:session'],
+      ['DELETE:/session/all', 'DELETE:session/all'],
+      ['POST:/session/refresh', 'POST:session/refresh'],
+      ['POST:/auth/sign_in', 'POST:auth/sign_in'],
+      ['GET:/auth/params', 'GET:auth/params'],
+    ])
+
+    this.PARAMETRIZED_AUTH_ROUTES = new Map([
+      ['PATCH:/users/([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})', 'users/{uuid}'],
+    ])
+  }
+
+  @httpPost('/items/sync', TYPES.AuthMiddleware)
+  async legacyItemsSync(request: Request, response: Response): Promise<void> {
+    await this.httpService.callLegacySyncingServer(request, response, request.path.substring(1), request.body)
+  }
+
+  @httpGet('/items/:item_id/revisions', TYPES.AuthMiddleware)
+  async legacyGetRevisions(request: Request, response: Response): Promise<void> {
+    await this.httpService.callLegacySyncingServer(request, response, request.path.substring(1), request.body)
+  }
+
+  @httpGet('/items/:item_id/revisions/:id', TYPES.AuthMiddleware)
+  async legacyGetRevision(request: Request, response: Response): Promise<void> {
+    await this.httpService.callLegacySyncingServer(request, response, request.path.substring(1), request.body)
+  }
+
+  @httpGet('/items/mfa/:userUuid')
+  async blockedMFARequest(): Promise<results.StatusCodeResult> {
+    return this.statusCode(401)
+  }
+
+  @httpDelete('/items/mfa/:userUuid')
+  async blockedMFARemoveRequest(): Promise<results.StatusCodeResult> {
+    return this.statusCode(401)
+  }
+
+  @all('*')
+  async legacyProxyToSyncingServer(request: Request, response: Response): Promise<void> {
+    if (request.path === '/') {
+      response.send('Welcome to the Standard Notes server infrastructure. Learn more at https://docs.standardnotes.com')
+
+      return
+    }
+
+    if (this.shouldBeRedirectedToAuthService(request)) {
+      const methodAndPath = this.getMethodAndPath(request)
+
+      request.method = methodAndPath.method
+      await this.httpService.callAuthServerWithLegacyFormat(request, response, methodAndPath.path, request.body)
+
+      return
+    }
+
+    await this.httpService.callLegacySyncingServer(request, response, request.path.substring(1), request.body)
+  }
+
+  private getMethodAndPath(request: Request): { method: string; path: string } {
+    const requestKey = `${request.method}:${request.path}`
+
+    if (this.AUTH_ROUTES.has(requestKey)) {
+      const legacyRoute = this.AUTH_ROUTES.get(requestKey) as string
+      const legacyRouteMethodAndPath = legacyRoute.split(':')
+
+      return {
+        method: legacyRouteMethodAndPath[0],
+        path: legacyRouteMethodAndPath[1],
+      }
+    }
+
+    for (const key of this.AUTH_ROUTES.keys()) {
+      const regExp = new RegExp(key)
+      const matches = regExp.exec(requestKey)
+      if (matches !== null) {
+        const legacyRoute = (this.AUTH_ROUTES.get(key) as string).replace('{uuid}', matches[1])
+        const legacyRouteMethodAndPath = legacyRoute.split(':')
+
+        return {
+          method: legacyRouteMethodAndPath[0],
+          path: legacyRouteMethodAndPath[1],
+        }
+      }
+    }
+
+    throw Error('could not find path for key')
+  }
+
+  private shouldBeRedirectedToAuthService(request: Request): boolean {
+    const requestKey = `${request.method}:${request.path}`
+
+    if (this.AUTH_ROUTES.has(requestKey)) {
+      return true
+    }
+
+    for (const key of this.PARAMETRIZED_AUTH_ROUTES.keys()) {
+      const regExp = new RegExp(key)
+      const matches = regExp.test(requestKey)
+      if (matches) {
+        return true
+      }
+    }
+
+    return false
+  }
+}

+ 31 - 0
packages/api-gateway/src/Controller/StatisticsMiddleware.ts

@@ -0,0 +1,31 @@
+import { NextFunction, Request, Response } from 'express'
+import { inject, injectable } from 'inversify'
+import { BaseMiddleware } from 'inversify-express-utils'
+import { Logger } from 'winston'
+import { StatisticsStoreInterface } from '@standardnotes/analytics'
+
+import TYPES from '../Bootstrap/Types'
+
+@injectable()
+export class StatisticsMiddleware extends BaseMiddleware {
+  constructor(
+    @inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
+    @inject(TYPES.Logger) private logger: Logger,
+  ) {
+    super()
+  }
+
+  async handler(request: Request, _response: Response, next: NextFunction): Promise<void> {
+    try {
+      const snjsVersion = request.headers['x-snjs-version'] ?? 'unknown'
+      await this.statisticsStore.incrementSNJSVersionUsage(snjsVersion as string)
+
+      const applicationVersion = request.headers['x-application-version'] ?? 'unknown'
+      await this.statisticsStore.incrementApplicationVersionUsage(applicationVersion as string)
+    } catch (error) {
+      this.logger.error(`Could not store analytics data: ${(error as Error).message}`)
+    }
+
+    return next()
+  }
+}

+ 120 - 0
packages/api-gateway/src/Controller/SubscriptionTokenAuthMiddleware.ts

@@ -0,0 +1,120 @@
+import { OfflineUserTokenData, CrossServiceTokenData } from '@standardnotes/auth'
+import { NextFunction, Request, Response } from 'express'
+import { inject, injectable } from 'inversify'
+import { BaseMiddleware } from 'inversify-express-utils'
+import { verify } from 'jsonwebtoken'
+import { AxiosError, AxiosInstance, AxiosResponse } from 'axios'
+import { Logger } from 'winston'
+import TYPES from '../Bootstrap/Types'
+import { TokenAuthenticationMethod } from './TokenAuthenticationMethod'
+
+@injectable()
+export class SubscriptionTokenAuthMiddleware extends BaseMiddleware {
+  constructor(
+    @inject(TYPES.HTTPClient) private httpClient: AxiosInstance,
+    @inject(TYPES.AUTH_SERVER_URL) private authServerUrl: string,
+    @inject(TYPES.AUTH_JWT_SECRET) private jwtSecret: string,
+    @inject(TYPES.Logger) private logger: Logger,
+  ) {
+    super()
+  }
+
+  async handler(request: Request, response: Response, next: NextFunction): Promise<void> {
+    const subscriptionToken = request.query.subscription_token
+    const email = request.headers['x-offline-email']
+    if (!subscriptionToken) {
+      response.status(401).send({
+        error: {
+          tag: 'invalid-auth',
+          message: 'Invalid login credentials.',
+        },
+      })
+
+      return
+    }
+
+    response.locals.tokenAuthenticationMethod = email
+      ? TokenAuthenticationMethod.OfflineSubscriptionToken
+      : TokenAuthenticationMethod.SubscriptionToken
+
+    try {
+      const url =
+        response.locals.tokenAuthenticationMethod == TokenAuthenticationMethod.OfflineSubscriptionToken
+          ? `${this.authServerUrl}/offline/subscription-tokens/${subscriptionToken}/validate`
+          : `${this.authServerUrl}/subscription-tokens/${subscriptionToken}/validate`
+
+      const authResponse = await this.httpClient.request({
+        method: 'POST',
+        headers: {
+          Accept: 'application/json',
+        },
+        data: {
+          email,
+        },
+        validateStatus: (status: number) => {
+          return status >= 200 && status < 500
+        },
+        url,
+      })
+
+      if (authResponse.status > 200) {
+        response.setHeader('content-type', authResponse.headers['content-type'])
+        response.status(authResponse.status).send(authResponse.data)
+
+        return
+      }
+
+      if (response.locals.tokenAuthenticationMethod == TokenAuthenticationMethod.OfflineSubscriptionToken) {
+        this.handleOfflineAuthTokenValidationResponse(response, authResponse)
+
+        return next()
+      }
+
+      this.handleAuthTokenValidationResponse(response, authResponse)
+
+      return next()
+    } catch (error) {
+      const errorMessage = (error as AxiosError).isAxiosError
+        ? JSON.stringify((error as AxiosError).response?.data)
+        : (error as Error).message
+
+      this.logger.error(
+        `Could not pass the request to ${this.authServerUrl}/subscription-tokens/${subscriptionToken}/validate on underlying service: ${errorMessage}`,
+      )
+
+      this.logger.debug('Response error: %O', (error as AxiosError).response ?? error)
+
+      if ((error as AxiosError).response?.headers['content-type']) {
+        response.setHeader('content-type', (error as AxiosError).response?.headers['content-type'] as string)
+      }
+
+      const errorCode = (error as AxiosError).isAxiosError ? +((error as AxiosError).code as string) : 500
+
+      response.status(errorCode).send(errorMessage)
+
+      return
+    }
+  }
+
+  private handleOfflineAuthTokenValidationResponse(response: Response, authResponse: AxiosResponse) {
+    response.locals.offlineAuthToken = authResponse.data.authToken
+
+    const decodedToken = <OfflineUserTokenData>(
+      verify(authResponse.data.authToken, this.jwtSecret, { algorithms: ['HS256'] })
+    )
+
+    response.locals.offlineUserEmail = decodedToken.userEmail
+    response.locals.offlineFeaturesToken = decodedToken.featuresToken
+  }
+
+  private handleAuthTokenValidationResponse(response: Response, authResponse: AxiosResponse) {
+    response.locals.authToken = authResponse.data.authToken
+
+    const decodedToken = <CrossServiceTokenData>(
+      verify(authResponse.data.authToken, this.jwtSecret, { algorithms: ['HS256'] })
+    )
+
+    response.locals.userUuid = decodedToken.user.uuid
+    response.locals.roles = decodedToken.roles
+  }
+}

+ 4 - 0
packages/api-gateway/src/Controller/TokenAuthenticationMethod.ts

@@ -0,0 +1,4 @@
+export enum TokenAuthenticationMethod {
+  OfflineSubscriptionToken = 'OfflineSubscriptionToken',
+  SubscriptionToken = 'SubscriptionToken',
+}

+ 52 - 0
packages/api-gateway/src/Controller/v1/ActionsController.ts

@@ -0,0 +1,52 @@
+import { Request, Response } from 'express'
+import { inject } from 'inversify'
+import { BaseHttpController, controller, httpGet, httpPost } from 'inversify-express-utils'
+import TYPES from '../../Bootstrap/Types'
+import { HttpServiceInterface } from '../../Service/Http/HttpServiceInterface'
+
+@controller('/v1', TYPES.StatisticsMiddleware)
+export class ActionsController extends BaseHttpController {
+  constructor(@inject(TYPES.HTTPService) private httpService: HttpServiceInterface) {
+    super()
+  }
+
+  @httpPost('/login')
+  async login(request: Request, response: Response): Promise<void> {
+    await this.httpService.callAuthServer(request, response, 'auth/sign_in', request.body)
+  }
+
+  @httpGet('/login-params')
+  async loginParams(request: Request, response: Response): Promise<void> {
+    await this.httpService.callAuthServer(request, response, 'auth/params', request.body)
+  }
+
+  @httpPost('/logout')
+  async logout(request: Request, response: Response): Promise<void> {
+    await this.httpService.callAuthServer(request, response, 'auth/sign_out', request.body)
+  }
+
+  @httpGet('/auth/methods')
+  async methods(request: Request, response: Response): Promise<void> {
+    await this.httpService.callAuthServer(request, response, 'auth/methods', request.body)
+  }
+
+  @httpGet('/failed-backups-emails/mute/:settingUuid')
+  async muteFailedBackupsEmails(request: Request, response: Response): Promise<void> {
+    await this.httpService.callAuthServer(
+      request,
+      response,
+      `internal/settings/email_backup/${request.params.settingUuid}/mute`,
+      request.body,
+    )
+  }
+
+  @httpGet('/sign-in-emails/mute/:settingUuid')
+  async muteSignInEmails(request: Request, response: Response): Promise<void> {
+    await this.httpService.callAuthServer(
+      request,
+      response,
+      `internal/settings/sign_in/${request.params.settingUuid}/mute`,
+      request.body,
+    )
+  }
+}

+ 18 - 0
packages/api-gateway/src/Controller/v1/FilesController.ts

@@ -0,0 +1,18 @@
+import { Request, Response } from 'express'
+import { inject } from 'inversify'
+import { BaseHttpController, controller, httpPost } from 'inversify-express-utils'
+
+import TYPES from '../../Bootstrap/Types'
+import { HttpServiceInterface } from '../../Service/Http/HttpServiceInterface'
+
+@controller('/v1/files', TYPES.StatisticsMiddleware)
+export class FilesController extends BaseHttpController {
+  constructor(@inject(TYPES.HTTPService) private httpService: HttpServiceInterface) {
+    super()
+  }
+
+  @httpPost('/valet-tokens', TYPES.AuthMiddleware)
+  async createToken(request: Request, response: Response): Promise<void> {
+    await this.httpService.callAuthServer(request, response, 'valet-tokens', request.body)
+  }
+}

+ 17 - 0
packages/api-gateway/src/Controller/v1/InvoicesController.ts

@@ -0,0 +1,17 @@
+import { Request, Response } from 'express'
+import { BaseHttpController, controller, httpPost } from 'inversify-express-utils'
+import { inject } from 'inversify'
+import TYPES from '../../Bootstrap/Types'
+import { HttpServiceInterface } from '../../Service/Http/HttpServiceInterface'
+
+@controller('/v1', TYPES.StatisticsMiddleware)
+export class InvoicesController extends BaseHttpController {
+  constructor(@inject(TYPES.HTTPService) private httpService: HttpServiceInterface) {
+    super()
+  }
+
+  @httpPost('/invoices/send-latest', TYPES.SubscriptionTokenAuthMiddleware)
+  async sendLatestInvoice(request: Request, response: Response): Promise<void> {
+    await this.httpService.callPaymentsServer(request, response, 'api/pro_users/send-invoice', request.body)
+  }
+}

+ 27 - 0
packages/api-gateway/src/Controller/v1/ItemsController.ts

@@ -0,0 +1,27 @@
+import { Request, Response } from 'express'
+import { inject } from 'inversify'
+import { BaseHttpController, controller, httpGet, httpPost } from 'inversify-express-utils'
+import TYPES from '../../Bootstrap/Types'
+import { HttpServiceInterface } from '../../Service/Http/HttpServiceInterface'
+
+@controller('/v1/items', TYPES.StatisticsMiddleware, TYPES.AuthMiddleware)
+export class ItemsController extends BaseHttpController {
+  constructor(@inject(TYPES.HTTPService) private httpService: HttpServiceInterface) {
+    super()
+  }
+
+  @httpPost('/')
+  async sync(request: Request, response: Response): Promise<void> {
+    await this.httpService.callSyncingServer(request, response, 'items/sync', request.body)
+  }
+
+  @httpPost('/check-integrity')
+  async checkIntegrity(request: Request, response: Response): Promise<void> {
+    await this.httpService.callSyncingServer(request, response, 'items/check-integrity', request.body)
+  }
+
+  @httpGet('/:uuid')
+  async getItem(request: Request, response: Response): Promise<void> {
+    await this.httpService.callSyncingServer(request, response, `items/${request.params.uuid}`, request.body)
+  }
+}

+ 33 - 0
packages/api-gateway/src/Controller/v1/OfflineController.ts

@@ -0,0 +1,33 @@
+import { Request, Response } from 'express'
+import { inject } from 'inversify'
+import { BaseHttpController, controller, httpGet, httpPost } from 'inversify-express-utils'
+
+import TYPES from '../../Bootstrap/Types'
+import { HttpServiceInterface } from '../../Service/Http/HttpServiceInterface'
+
+@controller('/v1/offline', TYPES.StatisticsMiddleware)
+export class OfflineController extends BaseHttpController {
+  constructor(@inject(TYPES.HTTPService) private httpService: HttpServiceInterface) {
+    super()
+  }
+
+  @httpGet('/features')
+  async getOfflineFeatures(request: Request, response: Response): Promise<void> {
+    await this.httpService.callAuthServer(request, response, 'offline/features', request.body)
+  }
+
+  @httpPost('/subscription-tokens')
+  async createOfflineSubscriptionToken(request: Request, response: Response): Promise<void> {
+    await this.httpService.callAuthServer(request, response, 'offline/subscription-tokens', request.body)
+  }
+
+  @httpPost('/payments/stripe-setup-intent')
+  async createStripeSetupIntent(request: Request, response: Response): Promise<void> {
+    await this.httpService.callPaymentsServer(
+      request,
+      response,
+      'api/pro_users/stripe-setup-intent/offline',
+      request.body,
+    )
+  }
+}

+ 162 - 0
packages/api-gateway/src/Controller/v1/PaymentsController.ts

@@ -0,0 +1,162 @@
+import { Request, Response } from 'express'
+import { inject } from 'inversify'
+import { all, BaseHttpController, controller, httpDelete, httpGet, httpPost } from 'inversify-express-utils'
+import TYPES from '../../Bootstrap/Types'
+import { HttpServiceInterface } from '../../Service/Http/HttpServiceInterface'
+
+@controller('/v1', TYPES.StatisticsMiddleware)
+export class PaymentsController extends BaseHttpController {
+  constructor(@inject(TYPES.HTTPService) private httpService: HttpServiceInterface) {
+    super()
+  }
+
+  @httpGet('/downloads')
+  async downloads(request: Request, response: Response): Promise<void> {
+    await this.httpService.callPaymentsServer(request, response, 'api/downloads', request.body)
+  }
+
+  @httpGet('/downloads/download-info')
+  async downloadInfo(request: Request, response: Response): Promise<void> {
+    await this.httpService.callPaymentsServer(request, response, 'api/downloads/download-info', request.body)
+  }
+
+  @httpGet('/downloads/platforms')
+  async platformDownloads(request: Request, response: Response): Promise<void> {
+    await this.httpService.callPaymentsServer(request, response, 'api/downloads/platforms', request.body)
+  }
+
+  @httpGet('/help/categories')
+  async categoriesHelp(request: Request, response: Response): Promise<void> {
+    await this.httpService.callPaymentsServer(request, response, 'api/help/categories', request.body)
+  }
+
+  @httpGet('/knowledge/categories')
+  async categoriesKnowledge(request: Request, response: Response): Promise<void> {
+    await this.httpService.callPaymentsServer(request, response, 'api/knowledge/categories', request.body)
+  }
+
+  @httpGet('/extensions')
+  async extensions(request: Request, response: Response): Promise<void> {
+    await this.httpService.callPaymentsServer(request, response, 'api/extensions', request.body)
+  }
+
+  @httpPost('/subscriptions/tiered', TYPES.SubscriptionTokenAuthMiddleware)
+  async createTieredSubscription(request: Request, response: Response): Promise<void> {
+    await this.httpService.callPaymentsServer(request, response, 'api/subscriptions/tiered', request.body)
+  }
+
+  @all('/subscriptions(/*)?')
+  async subscriptions(request: Request, response: Response): Promise<void> {
+    await this.httpService.callPaymentsServer(request, response, request.path.replace('v1', 'api'), request.body)
+  }
+
+  @httpGet('/reset/validate')
+  async validateReset(request: Request, response: Response): Promise<void> {
+    await this.httpService.callPaymentsServer(request, response, 'api/reset/validate', request.body)
+  }
+
+  @httpDelete('/reset')
+  async reset(request: Request, response: Response): Promise<void> {
+    await this.httpService.callPaymentsServer(request, response, 'api/reset', request.body)
+  }
+
+  @httpPost('/reset')
+  async resetRequest(request: Request, response: Response): Promise<void> {
+    await this.httpService.callPaymentsServer(request, response, 'api/reset', request.body)
+  }
+
+  @httpPost('/user-registration')
+  async userRegistration(request: Request, response: Response): Promise<void> {
+    await this.httpService.callPaymentsServer(request, response, 'admin/events/registration', request.body)
+  }
+
+  @httpPost('/admin/graphql')
+  async adminGraphql(request: Request, response: Response): Promise<void> {
+    await this.httpService.callPaymentsServer(request, response, 'admin/graphql', request.body)
+  }
+
+  @httpPost('/admin/auth/login')
+  async adminLogin(request: Request, response: Response): Promise<void> {
+    await this.httpService.callPaymentsServer(request, response, 'admin/auth/login', request.body)
+  }
+
+  @httpPost('/admin/auth/logout')
+  async adminLogout(request: Request, response: Response): Promise<void> {
+    await this.httpService.callPaymentsServer(request, response, 'admin/auth/logout', request.body)
+  }
+
+  @httpPost('/students')
+  async students(request: Request, response: Response): Promise<void> {
+    await this.httpService.callPaymentsServer(request, response, 'api/students', request.body)
+  }
+
+  @httpPost('/students/:token/approve')
+  async studentsApprove(request: Request, response: Response): Promise<void> {
+    await this.httpService.callPaymentsServer(
+      request,
+      response,
+      `api/students/${request.params.token}/approve`,
+      request.body,
+    )
+  }
+
+  @httpPost('/email_subscriptions/:token/less')
+  async subscriptionsLess(request: Request, response: Response): Promise<void> {
+    await this.httpService.callPaymentsServer(
+      request,
+      response,
+      `api/email_subscriptions/${request.params.token}/less`,
+      request.body,
+    )
+  }
+
+  @httpPost('/email_subscriptions/:token/more')
+  async subscriptionsMore(request: Request, response: Response): Promise<void> {
+    await this.httpService.callPaymentsServer(
+      request,
+      response,
+      `api/email_subscriptions/${request.params.token}/more`,
+      request.body,
+    )
+  }
+
+  @httpPost('/email_subscriptions/:token/mute/:campaignId')
+  async subscriptionsMute(request: Request, response: Response): Promise<void> {
+    await this.httpService.callPaymentsServer(
+      request,
+      response,
+      `api/email_subscriptions/${request.params.token}/mute/${request.params.campaignId}`,
+      request.body,
+    )
+  }
+
+  @httpPost('/email_subscriptions/:token/unsubscribe')
+  async subscriptionsUnsubscribe(request: Request, response: Response): Promise<void> {
+    await this.httpService.callPaymentsServer(
+      request,
+      response,
+      `api/email_subscriptions/${request.params.token}/unsubscribe`,
+      request.body,
+    )
+  }
+
+  @httpPost('/payments/stripe-setup-intent', TYPES.SubscriptionTokenAuthMiddleware)
+  async createStripeSetupIntent(request: Request, response: Response): Promise<void> {
+    await this.httpService.callPaymentsServer(request, response, 'api/pro_users/stripe-setup-intent', request.body)
+  }
+
+  @httpGet('/pro_users/cp-prepayment-info', TYPES.SubscriptionTokenAuthMiddleware)
+  async coinpaymentsPrepaymentInfo(request: Request, response: Response): Promise<void> {
+    await this.httpService.callPaymentsServer(request, response, 'api/pro_users/cp-prepayment-info', request.body)
+  }
+
+  @all('/pro_users(/*)?')
+  async proUsers(request: Request, response: Response): Promise<void> {
+    await this.httpService.callPaymentsServer(request, response, request.path.replace('v1', 'api'), request.body)
+  }
+
+  @all('/refunds')
+  async refunds(request: Request, response: Response): Promise<void> {
+    await this.httpService.callPaymentsServer(request, response, 'api/refunds', request.body)
+  }
+}

+ 35 - 0
packages/api-gateway/src/Controller/v1/RevisionsController.ts

@@ -0,0 +1,35 @@
+import { Request, Response } from 'express'
+import { inject } from 'inversify'
+import { BaseHttpController, controller, httpDelete, httpGet } from 'inversify-express-utils'
+import TYPES from '../../Bootstrap/Types'
+import { HttpServiceInterface } from '../../Service/Http/HttpServiceInterface'
+
+@controller('/v1/items/:item_id/revisions', TYPES.StatisticsMiddleware, TYPES.AuthMiddleware)
+export class RevisionsController extends BaseHttpController {
+  constructor(@inject(TYPES.HTTPService) private httpService: HttpServiceInterface) {
+    super()
+  }
+
+  @httpGet('/')
+  async getRevisions(request: Request, response: Response): Promise<void> {
+    await this.httpService.callSyncingServer(request, response, `items/${request.params.item_id}/revisions`)
+  }
+
+  @httpGet('/:id')
+  async getRevision(request: Request, response: Response): Promise<void> {
+    await this.httpService.callSyncingServer(
+      request,
+      response,
+      `items/${request.params.item_id}/revisions/${request.params.id}`,
+    )
+  }
+
+  @httpDelete('/:id')
+  async deleteRevision(request: Request, response: Response): Promise<void> {
+    await this.httpService.callSyncingServer(
+      request,
+      response,
+      `items/${request.params.item_id}/revisions/${request.params.id}`,
+    )
+  }
+}

+ 34 - 0
packages/api-gateway/src/Controller/v1/SessionsController.ts

@@ -0,0 +1,34 @@
+import { Request, Response } from 'express'
+import { inject } from 'inversify'
+import { BaseHttpController, controller, httpDelete, httpGet, httpPost } from 'inversify-express-utils'
+import TYPES from '../../Bootstrap/Types'
+import { HttpServiceInterface } from '../../Service/Http/HttpServiceInterface'
+
+@controller('/v1/sessions', TYPES.StatisticsMiddleware)
+export class SessionsController extends BaseHttpController {
+  constructor(@inject(TYPES.HTTPService) private httpService: HttpServiceInterface) {
+    super()
+  }
+
+  @httpGet('/', TYPES.AuthMiddleware)
+  async getSessions(request: Request, response: Response): Promise<void> {
+    await this.httpService.callAuthServer(request, response, 'sessions')
+  }
+
+  @httpDelete('/:uuid', TYPES.AuthMiddleware)
+  async deleteSession(request: Request, response: Response): Promise<void> {
+    await this.httpService.callAuthServer(request, response, 'session', {
+      uuid: request.params.uuid,
+    })
+  }
+
+  @httpDelete('/', TYPES.AuthMiddleware)
+  async deleteSessions(request: Request, response: Response): Promise<void> {
+    await this.httpService.callAuthServer(request, response, 'session/all')
+  }
+
+  @httpPost('/refresh')
+  async refreshSession(request: Request, response: Response): Promise<void> {
+    await this.httpService.callAuthServer(request, response, 'session/refresh', request.body)
+  }
+}

+ 42 - 0
packages/api-gateway/src/Controller/v1/SubscriptionInvitesController.ts

@@ -0,0 +1,42 @@
+import { Request, Response } from 'express'
+import { inject } from 'inversify'
+import { BaseHttpController, controller, httpDelete, httpGet, httpPost } from 'inversify-express-utils'
+
+import TYPES from '../../Bootstrap/Types'
+import { HttpServiceInterface } from '../../Service/Http/HttpServiceInterface'
+
+@controller('/v1/subscription-invites', TYPES.StatisticsMiddleware)
+export class SubscriptionInvitesController extends BaseHttpController {
+  constructor(@inject(TYPES.HTTPService) private httpService: HttpServiceInterface) {
+    super()
+  }
+
+  @httpPost('/', TYPES.AuthMiddleware)
+  async inviteToSubscriptionSharing(request: Request, response: Response): Promise<void> {
+    await this.httpService.callAuthServer(request, response, 'subscription-invites', request.body)
+  }
+
+  @httpGet('/', TYPES.AuthMiddleware)
+  async listInvites(request: Request, response: Response): Promise<void> {
+    await this.httpService.callAuthServer(request, response, 'subscription-invites', request.body)
+  }
+
+  @httpDelete('/:inviteUuid', TYPES.AuthMiddleware)
+  async cancelSubscriptionSharing(request: Request, response: Response): Promise<void> {
+    await this.httpService.callAuthServer(request, response, `subscription-invites/${request.params.inviteUuid}`)
+  }
+
+  @httpGet('/:inviteUuid/accept')
+  async acceptInvite(request: Request, response: Response): Promise<void> {
+    await this.httpService.callAuthServer(request, response, `subscription-invites/${request.params.inviteUuid}/accept`)
+  }
+
+  @httpGet('/:inviteUuid/decline')
+  async declineInvite(request: Request, response: Response): Promise<void> {
+    await this.httpService.callAuthServer(
+      request,
+      response,
+      `subscription-invites/${request.params.inviteUuid}/decline`,
+    )
+  }
+}

+ 18 - 0
packages/api-gateway/src/Controller/v1/TokensController.ts

@@ -0,0 +1,18 @@
+import { Request, Response } from 'express'
+import { inject } from 'inversify'
+import { BaseHttpController, controller, httpPost } from 'inversify-express-utils'
+
+import TYPES from '../../Bootstrap/Types'
+import { HttpServiceInterface } from '../../Service/Http/HttpServiceInterface'
+
+@controller('/v1/subscription-tokens', TYPES.StatisticsMiddleware)
+export class TokensController extends BaseHttpController {
+  constructor(@inject(TYPES.HTTPService) private httpService: HttpServiceInterface) {
+    super()
+  }
+
+  @httpPost('/', TYPES.AuthMiddleware)
+  async createToken(request: Request, response: Response): Promise<void> {
+    await this.httpService.callAuthServer(request, response, 'subscription-tokens', request.body)
+  }
+}

+ 150 - 0
packages/api-gateway/src/Controller/v1/UsersController.ts

@@ -0,0 +1,150 @@
+import { Request, Response } from 'express'
+import { inject } from 'inversify'
+import {
+  all,
+  BaseHttpController,
+  controller,
+  httpDelete,
+  httpGet,
+  httpPatch,
+  httpPost,
+  httpPut,
+  results,
+} from 'inversify-express-utils'
+import { Logger } from 'winston'
+import TYPES from '../../Bootstrap/Types'
+import { HttpServiceInterface } from '../../Service/Http/HttpServiceInterface'
+import { TokenAuthenticationMethod } from '../TokenAuthenticationMethod'
+
+@controller('/v1/users', TYPES.StatisticsMiddleware)
+export class UsersController extends BaseHttpController {
+  constructor(
+    @inject(TYPES.HTTPService) private httpService: HttpServiceInterface,
+    @inject(TYPES.Logger) private logger: Logger,
+  ) {
+    super()
+  }
+
+  @httpPost('/claim-account')
+  async claimAccount(request: Request, response: Response): Promise<void> {
+    await this.httpService.callPaymentsServer(request, response, 'api/pro_users/claim-account', request.body)
+  }
+
+  @httpPost('/send-activation-code', TYPES.SubscriptionTokenAuthMiddleware)
+  async sendActivationCode(request: Request, response: Response): Promise<void> {
+    await this.httpService.callPaymentsServer(request, response, 'api/pro_users/send-activation-code', request.body)
+  }
+
+  @httpPatch('/:userId', TYPES.AuthMiddleware)
+  async updateUser(request: Request, response: Response): Promise<void> {
+    await this.httpService.callAuthServer(request, response, `users/${request.params.userId}`, request.body)
+  }
+
+  @httpPut('/:userUuid/password', TYPES.AuthMiddleware)
+  async changePassword(request: Request, response: Response): Promise<void> {
+    this.logger.debug(
+      '[DEPRECATED] use endpoint /v1/users/:userUuid/attributes/credentials instead of /v1/users/:userUuid/password',
+    )
+
+    await this.httpService.callAuthServer(
+      request,
+      response,
+      `users/${request.params.userUuid}/attributes/credentials`,
+      request.body,
+    )
+  }
+
+  @httpPut('/:userUuid/attributes/credentials', TYPES.AuthMiddleware)
+  async changeCredentials(request: Request, response: Response): Promise<void> {
+    await this.httpService.callAuthServer(
+      request,
+      response,
+      `users/${request.params.userUuid}/attributes/credentials`,
+      request.body,
+    )
+  }
+
+  @httpGet('/:userId/params', TYPES.AuthMiddleware)
+  async getKeyParams(request: Request, response: Response): Promise<void> {
+    await this.httpService.callAuthServer(request, response, 'auth/params')
+  }
+
+  @all('/:userId/mfa', TYPES.AuthMiddleware)
+  async blockMFA(): Promise<results.StatusCodeResult> {
+    return this.statusCode(401)
+  }
+
+  @httpPost('/:userUuid/integrations/listed', TYPES.AuthMiddleware)
+  async createListedAccount(request: Request, response: Response): Promise<void> {
+    await this.httpService.callAuthServer(request, response, 'listed', request.body)
+  }
+
+  @httpPost('/')
+  async register(request: Request, response: Response): Promise<void> {
+    await this.httpService.callAuthServer(request, response, 'auth', request.body)
+  }
+
+  @httpGet('/:userUuid/settings', TYPES.AuthMiddleware)
+  async listSettings(request: Request, response: Response): Promise<void> {
+    await this.httpService.callAuthServer(request, response, `users/${request.params.userUuid}/settings`)
+  }
+
+  @httpPut('/:userUuid/settings', TYPES.AuthMiddleware)
+  async putSetting(request: Request, response: Response): Promise<void> {
+    await this.httpService.callAuthServer(request, response, `users/${request.params.userUuid}/settings`, request.body)
+  }
+
+  @httpGet('/:userUuid/settings/:settingName', TYPES.AuthMiddleware)
+  async getSetting(request: Request, response: Response): Promise<void> {
+    await this.httpService.callAuthServer(
+      request,
+      response,
+      `users/${request.params.userUuid}/settings/${request.params.settingName}`,
+    )
+  }
+
+  @httpDelete('/:userUuid/settings/:settingName', TYPES.AuthMiddleware)
+  async deleteSetting(request: Request, response: Response): Promise<void> {
+    await this.httpService.callAuthServer(
+      request,
+      response,
+      `users/${request.params.userUuid}/settings/${request.params.settingName}`,
+      request.body,
+    )
+  }
+
+  @httpGet('/:userUuid/subscription-settings/:subscriptionSettingName', TYPES.AuthMiddleware)
+  async getSubscriptionSetting(request: Request, response: Response): Promise<void> {
+    await this.httpService.callAuthServer(
+      request,
+      response,
+      `users/${request.params.userUuid}/subscription-settings/${request.params.subscriptionSettingName}`,
+    )
+  }
+
+  @httpGet('/:userUuid/features', TYPES.AuthMiddleware)
+  async getFeatures(request: Request, response: Response): Promise<void> {
+    await this.httpService.callAuthServer(request, response, `users/${request.params.userUuid}/features`)
+  }
+
+  @httpGet('/:userUuid/subscription', TYPES.AuthMiddleware)
+  async getSubscription(request: Request, response: Response): Promise<void> {
+    await this.httpService.callAuthServer(request, response, `users/${request.params.userUuid}/subscription`)
+  }
+
+  @httpGet('/subscription', TYPES.SubscriptionTokenAuthMiddleware)
+  async getSubscriptionBySubscriptionToken(request: Request, response: Response): Promise<void> {
+    if (response.locals.tokenAuthenticationMethod === TokenAuthenticationMethod.OfflineSubscriptionToken) {
+      await this.httpService.callAuthServer(request, response, 'offline/users/subscription')
+
+      return
+    }
+
+    await this.httpService.callAuthServer(request, response, `users/${response.locals.userUuid}/subscription`)
+  }
+
+  @httpDelete('/:userUuid', TYPES.AuthMiddleware)
+  async deleteUser(request: Request, response: Response): Promise<void> {
+    await this.httpService.callPaymentsServer(request, response, 'api/account', request.body)
+  }
+}

+ 34 - 0
packages/api-gateway/src/Controller/v1/WebSocketsController.ts

@@ -0,0 +1,34 @@
+import { Request, Response } from 'express'
+import { inject } from 'inversify'
+import { BaseHttpController, controller, httpDelete, httpPost } from 'inversify-express-utils'
+import TYPES from '../../Bootstrap/Types'
+import { HttpServiceInterface } from '../../Service/Http/HttpServiceInterface'
+
+@controller('/v1/sockets')
+export class WebSocketsController extends BaseHttpController {
+  constructor(@inject(TYPES.HTTPService) private httpService: HttpServiceInterface) {
+    super()
+  }
+
+  @httpPost('/', TYPES.AuthMiddleware)
+  async createWebSocketConnection(request: Request, response: Response): Promise<void> {
+    if (!request.headers.connectionid) {
+      response.status(400).send('Missing connection id in the request')
+
+      return
+    }
+
+    await this.httpService.callAuthServer(request, response, `sockets/${request.headers.connectionid}`, request.body)
+  }
+
+  @httpDelete('/')
+  async deleteWebSocketConnection(request: Request, response: Response): Promise<void> {
+    if (!request.headers.connectionid) {
+      response.status(400).send('Missing connection id in the request')
+
+      return
+    }
+
+    await this.httpService.callAuthServer(request, response, `sockets/${request.headers.connectionid}`, request.body)
+  }
+}

+ 23 - 0
packages/api-gateway/src/Controller/v2/ActionsControllerV2.ts

@@ -0,0 +1,23 @@
+import { Request, Response } from 'express'
+import { inject } from 'inversify'
+import { BaseHttpController, controller, httpPost } from 'inversify-express-utils'
+
+import TYPES from '../../Bootstrap/Types'
+import { HttpServiceInterface } from '../../Service/Http/HttpServiceInterface'
+
+@controller('/v2', TYPES.StatisticsMiddleware)
+export class ActionsControllerV2 extends BaseHttpController {
+  constructor(@inject(TYPES.HTTPService) private httpService: HttpServiceInterface) {
+    super()
+  }
+
+  @httpPost('/login')
+  async login(request: Request, response: Response): Promise<void> {
+    await this.httpService.callAuthServer(request, response, 'auth/pkce_sign_in', request.body)
+  }
+
+  @httpPost('/login-params')
+  async loginParams(request: Request, response: Response): Promise<void> {
+    await this.httpService.callAuthServer(request, response, 'auth/pkce_params', request.body)
+  }
+}

+ 62 - 0
packages/api-gateway/src/Controller/v2/PaymentsControllerV2.ts

@@ -0,0 +1,62 @@
+import { Request, Response } from 'express'
+import { BaseHttpController, controller, httpDelete, httpGet, httpPatch, httpPost } from 'inversify-express-utils'
+import { inject } from 'inversify'
+import TYPES from '../../Bootstrap/Types'
+import { HttpServiceInterface } from '../../Service/Http/HttpServiceInterface'
+
+@controller('/v2', TYPES.StatisticsMiddleware)
+export class PaymentsControllerV2 extends BaseHttpController {
+  constructor(@inject(TYPES.HTTPService) private httpService: HttpServiceInterface) {
+    super()
+  }
+
+  @httpGet('/subscriptions')
+  async getSubscriptionsWithFeatures(request: Request, response: Response): Promise<void> {
+    await this.httpService.callPaymentsServer(request, response, 'api/subscriptions/features', request.body)
+  }
+
+  @httpGet('/subscriptions/tailored', TYPES.SubscriptionTokenAuthMiddleware)
+  async getTailoredSubscriptionsWithFeatures(request: Request, response: Response): Promise<void> {
+    await this.httpService.callPaymentsServer(request, response, 'api/subscriptions/features', request.body)
+  }
+
+  @httpGet('/subscriptions/deltas', TYPES.SubscriptionTokenAuthMiddleware)
+  async getSubscriptionDeltasForChangingPlan(request: Request, response: Response): Promise<void> {
+    await this.httpService.callPaymentsServer(request, response, 'api/subscriptions/deltas', request.body)
+  }
+
+  @httpPost('/subscriptions/deltas/apply', TYPES.SubscriptionTokenAuthMiddleware)
+  async applySubscriptionDelta(request: Request, response: Response): Promise<void> {
+    await this.httpService.callPaymentsServer(request, response, 'api/subscriptions/deltas/apply', request.body)
+  }
+
+  @httpGet('/subscriptions/:subscriptionId', TYPES.SubscriptionTokenAuthMiddleware)
+  async getSubscription(request: Request, response: Response): Promise<void> {
+    await this.httpService.callPaymentsServer(
+      request,
+      response,
+      `api/subscriptions/${request.params.subscriptionId}`,
+      request.body,
+    )
+  }
+
+  @httpDelete('/subscriptions/:subscriptionId', TYPES.SubscriptionTokenAuthMiddleware)
+  async cancelSubscription(request: Request, response: Response): Promise<void> {
+    await this.httpService.callPaymentsServer(
+      request,
+      response,
+      `api/subscriptions/${request.params.subscriptionId}`,
+      request.body,
+    )
+  }
+
+  @httpPatch('/subscriptions/:subscriptionId', TYPES.SubscriptionTokenAuthMiddleware)
+  async updateSubscription(request: Request, response: Response): Promise<void> {
+    await this.httpService.callPaymentsServer(
+      request,
+      response,
+      `api/subscriptions/${request.params.subscriptionId}`,
+      request.body,
+    )
+  }
+}

+ 46 - 0
packages/api-gateway/src/Infra/Redis/RedisCrossServiceTokenCache.ts

@@ -0,0 +1,46 @@
+import { inject, injectable } from 'inversify'
+import * as IORedis from 'ioredis'
+import TYPES from '../../Bootstrap/Types'
+
+import { CrossServiceTokenCacheInterface } from '../../Service/Cache/CrossServiceTokenCacheInterface'
+
+@injectable()
+export class RedisCrossServiceTokenCache implements CrossServiceTokenCacheInterface {
+  private readonly PREFIX = 'cst'
+  private readonly USER_CST_PREFIX = 'user-cst'
+
+  constructor(@inject(TYPES.Redis) private redisClient: IORedis.Redis) {}
+
+  async set(dto: {
+    authorizationHeaderValue: string
+    encodedCrossServiceToken: string
+    expiresAtInSeconds: number
+    userUuid: string
+  }): Promise<void> {
+    const pipeline = this.redisClient.pipeline()
+
+    pipeline.sadd(`${this.USER_CST_PREFIX}:${dto.userUuid}`, dto.authorizationHeaderValue)
+    pipeline.expireat(`${this.USER_CST_PREFIX}:${dto.userUuid}`, dto.expiresAtInSeconds)
+
+    pipeline.set(`${this.PREFIX}:${dto.authorizationHeaderValue}`, dto.encodedCrossServiceToken)
+    pipeline.expireat(`${this.PREFIX}:${dto.authorizationHeaderValue}`, dto.expiresAtInSeconds)
+
+    await pipeline.exec()
+  }
+
+  async get(authorizationHeaderValue: string): Promise<string | null> {
+    return this.redisClient.get(`${this.PREFIX}:${authorizationHeaderValue}`)
+  }
+
+  async invalidate(userUuid: string): Promise<void> {
+    const userAuthorizationHeaderValues = await this.redisClient.smembers(`${this.USER_CST_PREFIX}:${userUuid}`)
+
+    const pipeline = this.redisClient.pipeline()
+    for (const authorizationHeaderValue of userAuthorizationHeaderValues) {
+      pipeline.del(`${this.PREFIX}:${authorizationHeaderValue}`)
+    }
+    pipeline.del(`${this.USER_CST_PREFIX}:${userUuid}`)
+
+    await pipeline.exec()
+  }
+}

+ 10 - 0
packages/api-gateway/src/Service/Cache/CrossServiceTokenCacheInterface.ts

@@ -0,0 +1,10 @@
+export interface CrossServiceTokenCacheInterface {
+  set(dto: {
+    authorizationHeaderValue: string
+    encodedCrossServiceToken: string
+    expiresAtInSeconds: number
+    userUuid: string
+  }): Promise<void>
+  get(authorizationHeaderValue: string): Promise<string | null>
+  invalidate(userUuid: string): Promise<void>
+}

+ 231 - 0
packages/api-gateway/src/Service/Http/HttpService.ts

@@ -0,0 +1,231 @@
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+import { AxiosInstance, AxiosError, AxiosResponse, Method } from 'axios'
+import { Request, Response } from 'express'
+import { inject, injectable } from 'inversify'
+import { Logger } from 'winston'
+
+import TYPES from '../../Bootstrap/Types'
+import { CrossServiceTokenCacheInterface } from '../Cache/CrossServiceTokenCacheInterface'
+import { HttpServiceInterface } from './HttpServiceInterface'
+
+@injectable()
+export class HttpService implements HttpServiceInterface {
+  constructor(
+    @inject(TYPES.HTTPClient) private httpClient: AxiosInstance,
+    @inject(TYPES.AUTH_SERVER_URL) private authServerUrl: string,
+    @inject(TYPES.SYNCING_SERVER_JS_URL) private syncingServerJsUrl: string,
+    @inject(TYPES.PAYMENTS_SERVER_URL) private paymentsServerUrl: string,
+    @inject(TYPES.FILES_SERVER_URL) private filesServerUrl: string,
+    @inject(TYPES.HTTP_CALL_TIMEOUT) private httpCallTimeout: number,
+    @inject(TYPES.CrossServiceTokenCache) private crossServiceTokenCache: CrossServiceTokenCacheInterface,
+    @inject(TYPES.Logger) private logger: Logger,
+  ) {}
+
+  async callSyncingServer(
+    request: Request,
+    response: Response,
+    endpoint: string,
+    payload?: Record<string, unknown> | string,
+  ): Promise<void> {
+    await this.callServer(this.syncingServerJsUrl, request, response, endpoint, payload)
+  }
+
+  async callLegacySyncingServer(
+    request: Request,
+    response: Response,
+    endpoint: string,
+    payload?: Record<string, unknown> | string,
+  ): Promise<void> {
+    await this.callServerWithLegacyFormat(this.syncingServerJsUrl, request, response, endpoint, payload)
+  }
+
+  async callAuthServer(
+    request: Request,
+    response: Response,
+    endpoint: string,
+    payload?: Record<string, unknown> | string,
+  ): Promise<void> {
+    await this.callServer(this.authServerUrl, request, response, endpoint, payload)
+  }
+
+  async callPaymentsServer(
+    request: Request,
+    response: Response,
+    endpoint: string,
+    payload?: Record<string, unknown> | string,
+  ): Promise<void> {
+    if (!this.paymentsServerUrl) {
+      this.logger.debug('Payments Server URL not defined. Skipped request to Payments API.')
+
+      return
+    }
+    await this.callServerWithLegacyFormat(this.paymentsServerUrl, request, response, endpoint, payload)
+  }
+
+  async callAuthServerWithLegacyFormat(
+    request: Request,
+    response: Response,
+    endpoint: string,
+    payload?: Record<string, unknown> | string,
+  ): Promise<void> {
+    await this.callServerWithLegacyFormat(this.authServerUrl, request, response, endpoint, payload)
+  }
+
+  private async getServerResponse(
+    serverUrl: string,
+    request: Request,
+    response: Response,
+    endpoint: string,
+    payload?: Record<string, unknown> | string,
+  ): Promise<AxiosResponse | undefined> {
+    try {
+      const headers: Record<string, string> = {}
+      for (const headerName of Object.keys(request.headers)) {
+        headers[headerName] = request.headers[headerName] as string
+      }
+
+      delete headers.host
+      delete headers['content-length']
+
+      if (response.locals.authToken) {
+        headers['X-Auth-Token'] = response.locals.authToken
+      }
+
+      if (response.locals.offlineAuthToken) {
+        headers['X-Auth-Offline-Token'] = response.locals.offlineAuthToken
+      }
+
+      this.logger.debug(`Calling [${request.method}] ${serverUrl}/${endpoint},
+        headers: ${JSON.stringify(headers)},
+        query: ${JSON.stringify(request.query)},
+        payload: ${JSON.stringify(payload)}`)
+
+      const serviceResponse = await this.httpClient.request({
+        method: request.method as Method,
+        headers,
+        url: `${serverUrl}/${endpoint}`,
+        data: this.getRequestData(payload),
+        maxContentLength: Infinity,
+        maxBodyLength: Infinity,
+        params: request.query,
+        timeout: this.httpCallTimeout,
+        validateStatus: (status: number) => {
+          return status >= 200 && status < 500
+        },
+      })
+
+      if (serviceResponse.headers['x-invalidate-cache']) {
+        const userUuid = serviceResponse.headers['x-invalidate-cache']
+        await this.crossServiceTokenCache.invalidate(userUuid)
+      }
+
+      return serviceResponse
+    } catch (error) {
+      const errorMessage = (error as AxiosError).isAxiosError
+        ? JSON.stringify((error as AxiosError).response?.data)
+        : (error as Error).message
+
+      this.logger.error(`Could not pass the request to ${serverUrl}/${endpoint} on underlying service: ${errorMessage}`)
+
+      this.logger.debug('Response error: %O', (error as AxiosError).response ?? error)
+
+      if ((error as AxiosError).response?.headers['content-type']) {
+        response.setHeader('content-type', (error as AxiosError).response?.headers['content-type'] as string)
+      }
+
+      const errorCode = (error as AxiosError).isAxiosError ? +((error as AxiosError).code as string) : 500
+
+      response.status(errorCode).send(errorMessage)
+    }
+
+    return
+  }
+
+  private async callServer(
+    serverUrl: string,
+    request: Request,
+    response: Response,
+    endpoint: string,
+    payload?: Record<string, unknown> | string,
+  ): Promise<void> {
+    const serviceResponse = await this.getServerResponse(serverUrl, request, response, endpoint, payload)
+
+    this.logger.debug(`Response from underlying server: ${JSON.stringify(serviceResponse?.data)},
+      headers: ${JSON.stringify(serviceResponse?.headers)}`)
+
+    if (!serviceResponse) {
+      return
+    }
+
+    this.applyResponseHeaders(serviceResponse, response)
+
+    response.status(serviceResponse.status).send({
+      meta: {
+        auth: {
+          userUuid: response.locals.userUuid,
+          roles: response.locals.roles,
+        },
+        server: {
+          filesServerUrl: this.filesServerUrl,
+        },
+      },
+      data: serviceResponse.data,
+    })
+  }
+
+  private async callServerWithLegacyFormat(
+    serverUrl: string,
+    request: Request,
+    response: Response,
+    endpoint: string,
+    payload?: Record<string, unknown> | string,
+  ): Promise<void> {
+    const serviceResponse = await this.getServerResponse(serverUrl, request, response, endpoint, payload)
+
+    if (!serviceResponse) {
+      return
+    }
+
+    this.applyResponseHeaders(serviceResponse, response)
+
+    if (serviceResponse.request._redirectable._redirectCount > 0) {
+      response.status(302).redirect(serviceResponse.request.res.responseUrl)
+    } else {
+      response.status(serviceResponse.status).send(serviceResponse.data)
+    }
+  }
+
+  private getRequestData(
+    payload: Record<string, unknown> | string | undefined,
+  ): Record<string, unknown> | string | undefined {
+    if (
+      payload === '' ||
+      payload === null ||
+      payload === undefined ||
+      (typeof payload === 'object' && Object.keys(payload).length === 0)
+    ) {
+      return undefined
+    }
+
+    return payload
+  }
+
+  private applyResponseHeaders(serviceResponse: AxiosResponse, response: Response): void {
+    const returnedHeadersFromUnderlyingService = [
+      'access-control-allow-methods',
+      'access-control-allow-origin',
+      'access-control-expose-headers',
+      'authorization',
+      'content-type',
+      'x-ssjs-version',
+      'x-auth-version',
+    ]
+
+    returnedHeadersFromUnderlyingService.map((headerName) => {
+      const headerValue = serviceResponse.headers[headerName]
+      if (headerValue) {
+        response.setHeader(headerName, headerValue)
+      }
+    })
+  }
+}

+ 34 - 0
packages/api-gateway/src/Service/Http/HttpServiceInterface.ts

@@ -0,0 +1,34 @@
+import { Request, Response } from 'express'
+
+export interface HttpServiceInterface {
+  callAuthServer(
+    request: Request,
+    response: Response,
+    endpoint: string,
+    payload?: Record<string, unknown> | string,
+  ): Promise<void>
+  callAuthServerWithLegacyFormat(
+    request: Request,
+    response: Response,
+    endpoint: string,
+    payload?: Record<string, unknown> | string,
+  ): Promise<void>
+  callSyncingServer(
+    request: Request,
+    response: Response,
+    endpoint: string,
+    payload?: Record<string, unknown> | string,
+  ): Promise<void>
+  callLegacySyncingServer(
+    request: Request,
+    response: Response,
+    endpoint: string,
+    payload?: Record<string, unknown> | string,
+  ): Promise<void>
+  callPaymentsServer(
+    request: Request,
+    response: Response,
+    endpoint: string,
+    payload?: Record<string, unknown> | string,
+  ): Promise<void>
+}

+ 0 - 0
packages/api-gateway/test-setup.ts


+ 12 - 0
packages/api-gateway/tsconfig.json

@@ -0,0 +1,12 @@
+{
+  "extends": "../../tsconfig.json",
+  "compilerOptions": {
+    "composite": true,
+    "outDir": "./dist",
+  },
+  "include": [
+    "src/**/*",
+    "bin/**/*",
+  ],
+  "references": []
+}

+ 17 - 0
packages/api-gateway/wait-for.sh

@@ -0,0 +1,17 @@
+#!/bin/sh
+
+set -e
+
+host="$1"
+shift
+port="$1"
+shift
+cmd="$@"
+
+while ! nc -vz $host $port; do
+  >&2 echo "$host:$port is unavailable yet - waiting for it to start"
+  sleep 10
+done
+
+>&2 echo "$host:$port is up - executing command"
+exec $cmd

+ 1 - 1
packages/files/Dockerfile

@@ -20,6 +20,6 @@ COPY --from=builder /workspace ./
 # docker-build plugin runs `yarn pack` in all workspace dependencies and copies them to `packs` folder.
 COPY packs ./
 
-ENTRYPOINT [ "/workspace/packages/auth/docker/entrypoint.sh" ]
+ENTRYPOINT [ "/workspace/packages/files/docker/entrypoint.sh" ]
 
 CMD [ "start-web" ]

+ 1 - 1
packages/syncing-server/Dockerfile

@@ -20,6 +20,6 @@ COPY --from=builder /workspace ./
 # docker-build plugin runs `yarn pack` in all workspace dependencies and copies them to `packs` folder.
 COPY packs ./
 
-ENTRYPOINT [ "/workspace/packages/auth/docker/entrypoint.sh" ]
+ENTRYPOINT [ "/workspace/packages/syncing-server/docker/entrypoint.sh" ]
 
 CMD [ "start-web" ]

+ 3 - 0
tsconfig.json

@@ -33,6 +33,9 @@
     },
     {
       "path": "./packages/files"
+    },
+    {
+      "path": "./packages/api-gateway"
     }
   ]
 }

+ 104 - 3
yarn.lock

@@ -1836,13 +1836,55 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@standardnotes/analytics@npm:^1.6.0":
+"@standardnotes/analytics@npm:^1.4.0, @standardnotes/analytics@npm:^1.6.0":
   version: 1.6.0
   resolution: "@standardnotes/analytics@npm:1.6.0"
   checksum: 6a5e86152673ce9ddce43c52b5f699a1b3ba5141e58a944bda8eaa88fc2f4169df27239f82633226752bf3f10f9804f426721b9c919d10fbfbb51f952430eb1f
   languageName: node
   linkType: hard
 
+"@standardnotes/api-gateway@workspace:packages/api-gateway":
+  version: 0.0.0-use.local
+  resolution: "@standardnotes/api-gateway@workspace:packages/api-gateway"
+  dependencies:
+    "@newrelic/native-metrics": 7.0.2
+    "@newrelic/winston-enricher": ^2.1.0
+    "@sentry/node": ^6.16.1
+    "@standardnotes/analytics": ^1.4.0
+    "@standardnotes/auth": 3.19.2
+    "@standardnotes/domain-events": 2.29.0
+    "@standardnotes/domain-events-infra": 1.4.127
+    "@standardnotes/time": ^1.7.0
+    "@types/cors": ^2.8.9
+    "@types/express": ^4.17.11
+    "@types/ioredis": ^4.28.10
+    "@types/jest": ^28.1.3
+    "@types/jsonwebtoken": ^8.5.0
+    "@types/newrelic": ^7.0.1
+    "@types/prettyjson": ^0.0.29
+    "@typescript-eslint/eslint-plugin": ^5.29.0
+    aws-sdk: ^2.1160.0
+    axios: 0.24.0
+    cors: 2.8.5
+    dotenv: 8.2.0
+    eslint: ^8.14.0
+    eslint-plugin-prettier: ^4.0.0
+    express: 4.17.1
+    helmet: 4.4.1
+    inversify: ^6.0.1
+    inversify-express-utils: ^6.4.3
+    ioredis: ^5.0.6
+    jest: ^28.1.1
+    jsonwebtoken: 8.5.1
+    newrelic: 8.6.0
+    nodemon: ^2.0.16
+    prettyjson: 1.2.1
+    reflect-metadata: 0.1.13
+    ts-jest: ^28.0.1
+    winston: 3.3.3
+  languageName: unknown
+  linkType: soft
+
 "@standardnotes/api@npm:^1.1.13":
   version: 1.1.13
   resolution: "@standardnotes/api@npm:1.1.13"
@@ -1916,6 +1958,16 @@ __metadata:
   languageName: unknown
   linkType: soft
 
+"@standardnotes/auth@npm:3.19.2":
+  version: 3.19.2
+  resolution: "@standardnotes/auth@npm:3.19.2"
+  dependencies:
+    "@standardnotes/common": ^1.22.0
+    jsonwebtoken: ^8.5.1
+  checksum: 2e4b37b3034ca561e42525313c5ddf66249d27da4c423738241fb3f59f8ea990eafb60b113e648fa6904c171e3c86e93abffb7e72a52bb17b0eb6c9025427678
+  languageName: node
+  linkType: hard
+
 "@standardnotes/auth@npm:^3.18.9, @standardnotes/auth@npm:^3.19.2, @standardnotes/auth@npm:^3.19.3":
   version: 3.19.3
   resolution: "@standardnotes/auth@npm:3.19.3"
@@ -1943,6 +1995,21 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@standardnotes/domain-events-infra@npm:1.4.127":
+  version: 1.4.127
+  resolution: "@standardnotes/domain-events-infra@npm:1.4.127"
+  dependencies:
+    "@standardnotes/domain-events": ^2.29.0
+    aws-sdk: ^2.1082.0
+    ioredis: ^4.28.5
+    newrelic: ^8.8.0
+    reflect-metadata: ^0.1.13
+    sqs-consumer: ^5.6.0
+    winston: ^3.6.0
+  checksum: 54e37c296ff3b44adc8d425f89fd56eb42d435dded6b04a952ff77ebb867022585bb472635f488863c9f9b6493a04592593f7a0f2bfa1d86bb9354ba341d350f
+  languageName: node
+  linkType: hard
+
 "@standardnotes/domain-events-infra@npm:^1.4.135, @standardnotes/domain-events-infra@npm:^1.4.93, @standardnotes/domain-events-infra@npm:^1.5.0, @standardnotes/domain-events-infra@npm:^1.5.2":
   version: 1.5.2
   resolution: "@standardnotes/domain-events-infra@npm:1.5.2"
@@ -1958,7 +2025,17 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@standardnotes/domain-events@npm:^2.27.6, @standardnotes/domain-events@npm:^2.31.1, @standardnotes/domain-events@npm:^2.32.0, @standardnotes/domain-events@npm:^2.32.2":
+"@standardnotes/domain-events@npm:2.29.0":
+  version: 2.29.0
+  resolution: "@standardnotes/domain-events@npm:2.29.0"
+  dependencies:
+    "@standardnotes/auth": ^3.19.2
+    "@standardnotes/features": ^1.44.6
+  checksum: 1b68999e2a7a6a26a9ecd27638cbb878bd4abb987a1a3b254136af0bec5619d4f5f99ede1b27cf93561fd6d6453f645310b5decfe468c8dd644363caed48aeb9
+  languageName: node
+  linkType: hard
+
+"@standardnotes/domain-events@npm:^2.27.6, @standardnotes/domain-events@npm:^2.29.0, @standardnotes/domain-events@npm:^2.31.1, @standardnotes/domain-events@npm:^2.32.0, @standardnotes/domain-events@npm:^2.32.2":
   version: 2.32.2
   resolution: "@standardnotes/domain-events@npm:2.32.2"
   dependencies:
@@ -1980,7 +2057,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@standardnotes/features@npm:^1.36.3, @standardnotes/features@npm:^1.45.2, @standardnotes/features@npm:^1.45.5":
+"@standardnotes/features@npm:^1.36.3, @standardnotes/features@npm:^1.44.6, @standardnotes/features@npm:^1.45.2, @standardnotes/features@npm:^1.45.5":
   version: 1.45.5
   resolution: "@standardnotes/features@npm:1.45.5"
   dependencies:
@@ -3216,6 +3293,23 @@ __metadata:
   languageName: node
   linkType: hard
 
+"aws-sdk@npm:^2.1160.0":
+  version: 2.1160.0
+  resolution: "aws-sdk@npm:2.1160.0"
+  dependencies:
+    buffer: 4.9.2
+    events: 1.1.1
+    ieee754: 1.1.13
+    jmespath: 0.16.0
+    querystring: 0.2.0
+    sax: 1.2.1
+    url: 0.10.3
+    uuid: 8.0.0
+    xml2js: 0.4.19
+  checksum: b95647d4de6d07fc7b62ef409d1dd4a915f3711163a4da4057a3451d62b56818f4ea3d7970c2735af376c44a97d603190f2d85c2a11911eb6f97334ad0ace4a7
+  languageName: node
+  linkType: hard
+
 "axios@npm:0.24.0":
   version: 0.24.0
   resolution: "axios@npm:0.24.0"
@@ -5696,6 +5790,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"helmet@npm:4.4.1":
+  version: 4.4.1
+  resolution: "helmet@npm:4.4.1"
+  checksum: cfe385e185e1ef6e4cd2ade4c54e160b05dd0454f270a663c528a8666402cbcad14e0ff0df09567fa62b0b4ac3371bbd1c8a253f6e7af37656a22339fe98c869
+  languageName: node
+  linkType: hard
+
 "helmet@npm:^4.3.1":
   version: 4.6.0
   resolution: "helmet@npm:4.6.0"