浏览代码

feat: add grpc sessions validation server (#928)

* feat: add grpc sessions validation server

* feat: add client implementation on api gateway

* fix: response codes

* fix: errored response

* feat: add configuring grpc as optional service proxy

* fix env vars

* fix linter issue
Karol Sójko 1 年之前
父节点
当前提交
4f62cac213
共有 65 个文件被更改,包括 1415 次插入42 次删除
  1. 3 0
      .github/workflows/e2e-self-hosted.yml
  2. 95 0
      .pnp.cjs
  3. 二进制
      .yarn/cache/@grpc-grpc-js-npm-1.9.10-28317a9d2d-243cf994e6.zip
  4. 二进制
      .yarn/cache/@mapbox-node-pre-gyp-npm-1.0.11-5547f15a2b-59529a2444.zip
  5. 二进制
      .yarn/cache/@types-google-protobuf-npm-3.15.10-cbaa6c3e6c-29efde966f.zip
  6. 二进制
      .yarn/cache/google-protobuf-npm-3.15.8-75df975b6c-0b1ea24a55.zip
  7. 二进制
      .yarn/cache/google-protobuf-npm-3.21.2-7c82de39ab-b376c2e47f.zip
  8. 二进制
      .yarn/cache/grpc-tools-npm-1.12.4-956df6794d-56852c756f.zip
  9. 二进制
      .yarn/cache/grpc_tools_node_protoc_ts-npm-5.3.3-297a345c26-96fe57b04a.zip
  10. 1 0
      docker-compose.ci.yml
  11. 5 0
      docker/docker-entrypoint.sh
  12. 9 1
      package.json
  13. 1 0
      packages/api-gateway/.env.sample
  14. 1 1
      packages/api-gateway/bin/server.ts
  15. 2 0
      packages/api-gateway/package.json
  16. 49 7
      packages/api-gateway/src/Bootstrap/Container.ts
  17. 2 0
      packages/api-gateway/src/Bootstrap/Types.ts
  18. 1 1
      packages/api-gateway/src/Controller/AuthMiddleware.ts
  19. 1 1
      packages/api-gateway/src/Controller/LegacyController.ts
  20. 1 1
      packages/api-gateway/src/Controller/OptionalCrossServiceTokenMiddleware.ts
  21. 1 1
      packages/api-gateway/src/Controller/RequiredCrossServiceTokenMiddleware.ts
  22. 1 1
      packages/api-gateway/src/Controller/v1/ActionsController.ts
  23. 1 1
      packages/api-gateway/src/Controller/v1/AuthenticatorsController.ts
  24. 1 1
      packages/api-gateway/src/Controller/v1/FilesController.ts
  25. 1 1
      packages/api-gateway/src/Controller/v1/InvoicesController.ts
  26. 1 1
      packages/api-gateway/src/Controller/v1/ItemsController.ts
  27. 1 1
      packages/api-gateway/src/Controller/v1/MessagesController.ts
  28. 1 1
      packages/api-gateway/src/Controller/v1/OfflineController.ts
  29. 1 1
      packages/api-gateway/src/Controller/v1/PaymentsController.ts
  30. 1 1
      packages/api-gateway/src/Controller/v1/SessionsController.ts
  31. 1 1
      packages/api-gateway/src/Controller/v1/SharedVaultInvitesController.ts
  32. 1 1
      packages/api-gateway/src/Controller/v1/SharedVaultUsersController.ts
  33. 1 1
      packages/api-gateway/src/Controller/v1/SharedVaultsController.ts
  34. 1 1
      packages/api-gateway/src/Controller/v1/SubscriptionInvitesController.ts
  35. 1 1
      packages/api-gateway/src/Controller/v1/TokensController.ts
  36. 1 1
      packages/api-gateway/src/Controller/v1/UsersController.ts
  37. 1 1
      packages/api-gateway/src/Controller/v1/WebSocketsController.ts
  38. 1 1
      packages/api-gateway/src/Controller/v2/ActionsControllerV2.ts
  39. 1 1
      packages/api-gateway/src/Controller/v2/PaymentsControllerV2.ts
  40. 1 1
      packages/api-gateway/src/Controller/v2/RevisionsControllerV2.ts
  41. 1 1
      packages/api-gateway/src/Service/DirectCall/DirectCallServiceProxy.ts
  42. 1 1
      packages/api-gateway/src/Service/Http/HttpServiceProxy.ts
  43. 0 0
      packages/api-gateway/src/Service/Proxy/ServiceProxyInterface.ts
  44. 440 0
      packages/api-gateway/src/Service/gRPC/GRPCServiceProxy.ts
  45. 1 0
      packages/auth/.env.sample
  46. 40 2
      packages/auth/bin/server.ts
  47. 3 0
      packages/auth/package.json
  48. 74 0
      packages/auth/src/Infra/gRPC/SessionsServer.ts
  49. 1 1
      packages/files/bin/server.ts
  50. 41 0
      packages/grpc/lib/auth_grpc_pb.d.ts
  51. 44 0
      packages/grpc/lib/auth_grpc_pb.js
  52. 47 0
      packages/grpc/lib/auth_pb.d.ts
  53. 328 0
      packages/grpc/lib/auth_pb.js
  54. 2 0
      packages/grpc/lib/index.d.ts
  55. 18 0
      packages/grpc/lib/index.js
  56. 33 0
      packages/grpc/package.json
  57. 15 0
      packages/grpc/proto/auth.proto
  58. 20 0
      packages/grpc/scripts/build-protos.sh
  59. 9 0
      packages/grpc/tsconfig.json
  60. 3 1
      packages/home-server/src/Server/HomeServer.ts
  61. 1 1
      packages/revisions/bin/server.ts
  62. 1 1
      packages/syncing-server/bin/server.ts
  63. 1 1
      packages/websockets/bin/server.ts
  64. 3 0
      tsconfig.json
  65. 97 1
      yarn.lock

+ 3 - 0
.github/workflows/e2e-self-hosted.yml

@@ -17,6 +17,8 @@ jobs:
     name: (Self Hosting) E2E Test Suite
     strategy:
       fail-fast: false
+      matrix:
+        service_proxy_type: [http, grpc]
     runs-on: ubuntu-latest
 
     services:
@@ -42,6 +44,7 @@ jobs:
       env:
         DB_TYPE: mysql
         CACHE_TYPE: redis
+        SERVICE_PROXY_TYPE: ${{ matrix.service_proxy_type }}
 
     - name: Wait for server to start
       run: docker/is-available.sh http://localhost:3123 $(pwd)/logs

+ 95 - 0
.pnp.cjs

@@ -45,6 +45,10 @@ const RAW_RUNTIME_STATE =
       "name": "@standardnotes/files-server",\
       "reference": "workspace:packages/files"\
     },\
+    {\
+      "name": "@standardnotes/grpc",\
+      "reference": "workspace:packages/grpc"\
+    },\
     {\
       "name": "@standardnotes/home-server",\
       "reference": "workspace:packages/home-server"\
@@ -97,6 +101,7 @@ const RAW_RUNTIME_STATE =
     ["@standardnotes/domain-events", ["workspace:packages/domain-events"]],\
     ["@standardnotes/domain-events-infra", ["workspace:packages/domain-events-infra"]],\
     ["@standardnotes/files-server", ["workspace:packages/files"]],\
+    ["@standardnotes/grpc", ["workspace:packages/grpc"]],\
     ["@standardnotes/home-server", ["workspace:packages/home-server"]],\
     ["@standardnotes/predicates", ["workspace:packages/predicates"]],\
     ["@standardnotes/revisions-server", ["workspace:packages/revisions"]],\
@@ -2669,6 +2674,15 @@ const RAW_RUNTIME_STATE =
       }]\
     ]],\
     ["@grpc/grpc-js", [\
+      ["npm:1.9.10", {\
+        "packageLocation": "./.yarn/cache/@grpc-grpc-js-npm-1.9.10-28317a9d2d-243cf994e6.zip/node_modules/@grpc/grpc-js/",\
+        "packageDependencies": [\
+          ["@grpc/grpc-js", "npm:1.9.10"],\
+          ["@grpc/proto-loader", "npm:0.7.10"],\
+          ["@types/node", "npm:20.2.5"]\
+        ],\
+        "linkType": "HARD"\
+      }],\
       ["npm:1.9.5", {\
         "packageLocation": "./.yarn/cache/@grpc-grpc-js-npm-1.9.5-9b0cd6b5ed-5499d964d2.zip/node_modules/@grpc/grpc-js/",\
         "packageDependencies": [\
@@ -3570,6 +3584,22 @@ const RAW_RUNTIME_STATE =
           ["tar", "npm:6.1.15"]\
         ],\
         "linkType": "HARD"\
+      }],\
+      ["npm:1.0.11", {\
+        "packageLocation": "./.yarn/cache/@mapbox-node-pre-gyp-npm-1.0.11-5547f15a2b-59529a2444.zip/node_modules/@mapbox/node-pre-gyp/",\
+        "packageDependencies": [\
+          ["@mapbox/node-pre-gyp", "npm:1.0.11"],\
+          ["detect-libc", "npm:2.0.1"],\
+          ["https-proxy-agent", "npm:5.0.1"],\
+          ["make-dir", "npm:3.1.0"],\
+          ["node-fetch", "virtual:0f92dfe7f9dc4fd492639d4a5b7805c2b27442bf599fd4f370b22a7966ba078f5d4525e2a8e8af29369f20e1833ed084bd52be59679efaa6c1c6c10cdbcd8baa#npm:2.6.11"],\
+          ["nopt", "npm:5.0.0"],\
+          ["npmlog", "npm:5.0.1"],\
+          ["rimraf", "npm:3.0.2"],\
+          ["semver", "npm:7.5.1"],\
+          ["tar", "npm:6.1.15"]\
+        ],\
+        "linkType": "HARD"\
       }]\
     ]],\
     ["@nodelib/fs.scandir", [\
@@ -6371,9 +6401,11 @@ const RAW_RUNTIME_STATE =
         "packageLocation": "./packages/api-gateway/",\
         "packageDependencies": [\
           ["@standardnotes/api-gateway", "workspace:packages/api-gateway"],\
+          ["@grpc/grpc-js", "npm:1.9.10"],\
           ["@standardnotes/domain-core", "workspace:packages/domain-core"],\
           ["@standardnotes/domain-events", "workspace:packages/domain-events"],\
           ["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
+          ["@standardnotes/grpc", "workspace:packages/grpc"],\
           ["@standardnotes/security", "workspace:packages/security"],\
           ["@standardnotes/time", "workspace:packages/time"],\
           ["@types/cors", "npm:2.8.13"],\
@@ -6419,6 +6451,7 @@ const RAW_RUNTIME_STATE =
           ["@aws-sdk/client-sqs", "npm:3.427.0"],\
           ["@cbor-extract/cbor-extract-linux-arm64", "npm:2.1.1"],\
           ["@cbor-extract/cbor-extract-linux-x64", "npm:2.1.1"],\
+          ["@grpc/grpc-js", "npm:1.9.10"],\
           ["@simplewebauthn/server", "npm:8.1.1"],\
           ["@simplewebauthn/typescript-types", "npm:8.0.0"],\
           ["@standardnotes/api", "npm:1.26.26"],\
@@ -6427,6 +6460,7 @@ const RAW_RUNTIME_STATE =
           ["@standardnotes/domain-events", "workspace:packages/domain-events"],\
           ["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
           ["@standardnotes/features", "npm:1.59.7"],\
+          ["@standardnotes/grpc", "workspace:packages/grpc"],\
           ["@standardnotes/predicates", "workspace:packages/predicates"],\
           ["@standardnotes/responses", "npm:1.13.27"],\
           ["@standardnotes/security", "workspace:packages/security"],\
@@ -6639,6 +6673,21 @@ const RAW_RUNTIME_STATE =
         "linkType": "SOFT"\
       }]\
     ]],\
+    ["@standardnotes/grpc", [\
+      ["workspace:packages/grpc", {\
+        "packageLocation": "./packages/grpc/",\
+        "packageDependencies": [\
+          ["@standardnotes/grpc", "workspace:packages/grpc"],\
+          ["@grpc/grpc-js", "npm:1.9.10"],\
+          ["@types/google-protobuf", "npm:3.15.10"],\
+          ["google-protobuf", "npm:3.21.2"],\
+          ["grpc-tools", "npm:1.12.4"],\
+          ["grpc_tools_node_protoc_ts", "npm:5.3.3"],\
+          ["typescript", "patch:typescript@npm%3A5.0.4#optional!builtin<compat/typescript>::version=5.0.4&hash=b5f058"]\
+        ],\
+        "linkType": "SOFT"\
+      }]\
+    ]],\
     ["@standardnotes/home-server", [\
       ["workspace:packages/home-server", {\
         "packageLocation": "./packages/home-server/",\
@@ -7284,6 +7333,15 @@ const RAW_RUNTIME_STATE =
         "linkType": "HARD"\
       }]\
     ]],\
+    ["@types/google-protobuf", [\
+      ["npm:3.15.10", {\
+        "packageLocation": "./.yarn/cache/@types-google-protobuf-npm-3.15.10-cbaa6c3e6c-29efde966f.zip/node_modules/@types/google-protobuf/",\
+        "packageDependencies": [\
+          ["@types/google-protobuf", "npm:3.15.10"]\
+        ],\
+        "linkType": "HARD"\
+      }]\
+    ]],\
     ["@types/graceful-fs", [\
       ["npm:4.1.6", {\
         "packageLocation": "./.yarn/cache/@types-graceful-fs-npm-4.1.6-1eadcf742d-c3070ccdc9.zip/node_modules/@types/graceful-fs/",\
@@ -11296,6 +11354,22 @@ const RAW_RUNTIME_STATE =
         "linkType": "HARD"\
       }]\
     ]],\
+    ["google-protobuf", [\
+      ["npm:3.15.8", {\
+        "packageLocation": "./.yarn/cache/google-protobuf-npm-3.15.8-75df975b6c-0b1ea24a55.zip/node_modules/google-protobuf/",\
+        "packageDependencies": [\
+          ["google-protobuf", "npm:3.15.8"]\
+        ],\
+        "linkType": "HARD"\
+      }],\
+      ["npm:3.21.2", {\
+        "packageLocation": "./.yarn/cache/google-protobuf-npm-3.21.2-7c82de39ab-b376c2e47f.zip/node_modules/google-protobuf/",\
+        "packageDependencies": [\
+          ["google-protobuf", "npm:3.21.2"]\
+        ],\
+        "linkType": "HARD"\
+      }]\
+    ]],\
     ["graceful-fs", [\
       ["npm:4.2.11", {\
         "packageLocation": "./.yarn/cache/graceful-fs-npm-4.2.11-24bb648a68-bf152d0ed1.zip/node_modules/graceful-fs/",\
@@ -11314,6 +11388,27 @@ const RAW_RUNTIME_STATE =
         "linkType": "HARD"\
       }]\
     ]],\
+    ["grpc-tools", [\
+      ["npm:1.12.4", {\
+        "packageLocation": "./.yarn/unplugged/grpc-tools-npm-1.12.4-956df6794d/node_modules/grpc-tools/",\
+        "packageDependencies": [\
+          ["grpc-tools", "npm:1.12.4"],\
+          ["@mapbox/node-pre-gyp", "npm:1.0.11"]\
+        ],\
+        "linkType": "HARD"\
+      }]\
+    ]],\
+    ["grpc_tools_node_protoc_ts", [\
+      ["npm:5.3.3", {\
+        "packageLocation": "./.yarn/unplugged/grpc_tools_node_protoc_ts-npm-5.3.3-297a345c26/node_modules/grpc_tools_node_protoc_ts/",\
+        "packageDependencies": [\
+          ["grpc_tools_node_protoc_ts", "npm:5.3.3"],\
+          ["google-protobuf", "npm:3.15.8"],\
+          ["handlebars", "npm:4.7.7"]\
+        ],\
+        "linkType": "HARD"\
+      }]\
+    ]],\
     ["handlebars", [\
       ["npm:4.7.7", {\
         "packageLocation": "./.yarn/cache/handlebars-npm-4.7.7-a9ccfabf80-617b1e689b.zip/node_modules/handlebars/",\

二进制
.yarn/cache/@grpc-grpc-js-npm-1.9.10-28317a9d2d-243cf994e6.zip


二进制
.yarn/cache/@mapbox-node-pre-gyp-npm-1.0.11-5547f15a2b-59529a2444.zip


二进制
.yarn/cache/@types-google-protobuf-npm-3.15.10-cbaa6c3e6c-29efde966f.zip


二进制
.yarn/cache/google-protobuf-npm-3.15.8-75df975b6c-0b1ea24a55.zip


二进制
.yarn/cache/google-protobuf-npm-3.21.2-7c82de39ab-b376c2e47f.zip


二进制
.yarn/cache/grpc-tools-npm-1.12.4-956df6794d-56852c756f.zip


二进制
.yarn/cache/grpc_tools_node_protoc_ts-npm-5.3.3-297a345c26-96fe57b04a.zip


+ 1 - 0
docker-compose.ci.yml

@@ -26,6 +26,7 @@ services:
     ports:
       - 3123:3000
       - 3125:3104
+      - 50051:50051
     volumes:
       - ./logs:/var/lib/server/logs
     networks:

+ 5 - 0
docker/docker-entrypoint.sh

@@ -18,6 +18,10 @@ if [ -z "$AUTH_SERVER_PORT" ]; then
   export AUTH_SERVER_PORT=3103
 fi
 
+if [ -z "$AUTH_SERVER_GRPC_PORT" ]; then
+  export AUTH_SERVER_GRPC_PORT=50051
+fi
+
 export FILES_SERVER_PORT=3104
 
 if [ -z "$REVISIONS_SERVER_PORT" ]; then
@@ -353,6 +357,7 @@ export API_GATEWAY_VERSION=local
 
 export API_GATEWAY_SYNCING_SERVER_JS_URL=http://localhost:$SYNCING_SERVER_PORT
 export API_GATEWAY_AUTH_SERVER_URL=http://localhost:$AUTH_SERVER_PORT
+export API_GATEWAY_AUTH_SERVER_GRPC_URL=0.0.0.0:$AUTH_SERVER_GRPC_PORT
 export API_GATEWAY_REVISIONS_SERVER_URL=http://localhost:$REVISIONS_SERVER_PORT
 if [ -z "$PUBLIC_FILES_SERVER_URL" ]; then
   export PUBLIC_FILES_SERVER_URL=http://localhost:3125

+ 9 - 1
package.json

@@ -39,5 +39,13 @@
     "ts-node": "^10.9.1",
     "typescript": "^5.0.4"
   },
-  "packageManager": "yarn@4.0.0-rc.51"
+  "packageManager": "yarn@4.0.0-rc.51",
+  "dependenciesMeta": {
+    "grpc-tools@1.12.4": {
+      "unplugged": true
+    },
+    "grpc_tools_node_protoc_ts@5.3.3": {
+      "unplugged": true
+    }
+  }
 }

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

@@ -7,6 +7,7 @@ PORT=3000
 
 SYNCING_SERVER_JS_URL=http://syncing_server_js:3000
 AUTH_SERVER_URL=http://auth:3000
+AUTH_SERVER_GRPC_URL=http://auth:50051
 WEB_SOCKET_SERVER_URL=http://websockets:3000
 PAYMENTS_SERVER_URL=http://payments:3000
 FILES_SERVER_URL=http://files:3000

+ 1 - 1
packages/api-gateway/bin/server.ts

@@ -111,7 +111,7 @@ void container.load().then((container) => {
 
   const serverInstance = server.build().listen(env.get('PORT'))
 
-  const keepAliveTimeout = env.get('KEEP_ALIVE_TIMEOUT', true) ? +env.get('KEEP_ALIVE_TIMEOUT', true) : 5000
+  const keepAliveTimeout = env.get('HTTP_KEEP_ALIVE_TIMEOUT', true) ? +env.get('HTTP_KEEP_ALIVE_TIMEOUT', true) : 5000
 
   serverInstance.keepAliveTimeout = keepAliveTimeout
 

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

@@ -26,9 +26,11 @@
     "start": "yarn node dist/bin/server.js"
   },
   "dependencies": {
+    "@grpc/grpc-js": "^1.9.10",
     "@standardnotes/domain-core": "workspace:^",
     "@standardnotes/domain-events": "workspace:*",
     "@standardnotes/domain-events-infra": "workspace:*",
+    "@standardnotes/grpc": "workspace:^",
     "@standardnotes/security": "workspace:*",
     "@standardnotes/time": "workspace:*",
     "agentkeepalive": "^4.5.0",

+ 49 - 7
packages/api-gateway/src/Bootstrap/Container.ts

@@ -1,5 +1,6 @@
 import * as winston from 'winston'
 import * as AgentKeepAlive from 'agentkeepalive'
+import * as grpc from '@grpc/grpc-js'
 import axios, { AxiosInstance } from 'axios'
 import Redis from 'ioredis'
 import { Container } from 'inversify'
@@ -7,20 +8,22 @@ import { Timer, TimerInterface } from '@standardnotes/time'
 
 import { Env } from './Env'
 import { TYPES } from './Types'
-import { ServiceProxyInterface } from '../Service/Http/ServiceProxyInterface'
+import { ServiceProxyInterface } from '../Service/Proxy/ServiceProxyInterface'
 import { HttpServiceProxy } from '../Service/Http/HttpServiceProxy'
 import { SubscriptionTokenAuthMiddleware } from '../Controller/SubscriptionTokenAuthMiddleware'
 import { CrossServiceTokenCacheInterface } from '../Service/Cache/CrossServiceTokenCacheInterface'
 import { RedisCrossServiceTokenCache } from '../Infra/Redis/RedisCrossServiceTokenCache'
 import { WebSocketAuthMiddleware } from '../Controller/WebSocketAuthMiddleware'
 import { InMemoryCrossServiceTokenCache } from '../Infra/InMemory/InMemoryCrossServiceTokenCache'
-import { DirectCallServiceProxy } from '../Service/Proxy/DirectCallServiceProxy'
+import { DirectCallServiceProxy } from '../Service/DirectCall/DirectCallServiceProxy'
 import { ServiceContainerInterface } from '@standardnotes/domain-core'
 import { EndpointResolverInterface } from '../Service/Resolver/EndpointResolverInterface'
 import { EndpointResolver } from '../Service/Resolver/EndpointResolver'
 import { RequiredCrossServiceTokenMiddleware } from '../Controller/RequiredCrossServiceTokenMiddleware'
 import { OptionalCrossServiceTokenMiddleware } from '../Controller/OptionalCrossServiceTokenMiddleware'
 import { Transform } from 'stream'
+import { ISessionsClient, SessionsClient } from '@standardnotes/grpc'
+import { GRPCServiceProxy } from '../Service/gRPC/GRPCServiceProxy'
 
 export class ContainerConfigLoader {
   async load(configuration?: {
@@ -69,14 +72,16 @@ export class ContainerConfigLoader {
       container.bind(TYPES.ApiGateway_Redis).toConstantValue(redis)
     }
 
+    const httpAgentKeepAliveTimeout = env.get('HTTP_AGENT_KEEP_ALIVE_TIMEOUT', true)
+      ? +env.get('HTTP_AGENT_KEEP_ALIVE_TIMEOUT', true)
+      : 4_000
+
     container.bind<AxiosInstance>(TYPES.ApiGateway_HTTPClient).toConstantValue(
       axios.create({
         httpAgent: new AgentKeepAlive({
           keepAlive: true,
-          timeout: env.get('AGENT_KEEP_ALIVE_TIMEOUT', true) ? +env.get('AGENT_KEEP_ALIVE_TIMEOUT', true) : 8_000,
-          freeSocketTimeout: env.get('AGENT_KEEP_ALIVE_FREE_SOCKET_TIMEOUT', true)
-            ? +env.get('AGENT_KEEP_ALIVE_FREE_SOCKET_TIMEOUT', true)
-            : 4_000,
+          timeout: 2 * httpAgentKeepAliveTimeout,
+          freeSocketTimeout: httpAgentKeepAliveTimeout,
         }),
       }),
     )
@@ -124,7 +129,44 @@ export class ContainerConfigLoader {
           new DirectCallServiceProxy(configuration.serviceContainer, container.get(TYPES.ApiGateway_FILES_SERVER_URL)),
         )
     } else {
-      container.bind<ServiceProxyInterface>(TYPES.ApiGateway_ServiceProxy).to(HttpServiceProxy)
+      const isConfiguredForGRPCProxy = env.get('SERVICE_PROXY_TYPE', true) === 'grpc'
+      if (isConfiguredForGRPCProxy) {
+        container.bind(TYPES.ApiGateway_AUTH_SERVER_GRPC_URL).toConstantValue(env.get('AUTH_SERVER_GRPC_URL'))
+        const grpcAgentKeepAliveTimeout = env.get('GRPC_AGENT_KEEP_ALIVE_TIMEOUT', true)
+          ? +env.get('GRPC_AGENT_KEEP_ALIVE_TIMEOUT', true)
+          : 8_000
+        container.bind<ISessionsClient>(TYPES.ApiGateway_GRPCSessionsClient).toConstantValue(
+          new SessionsClient(
+            container.get<string>(TYPES.ApiGateway_AUTH_SERVER_GRPC_URL),
+            grpc.credentials.createInsecure(),
+            {
+              'grpc.keepalive_time_ms': grpcAgentKeepAliveTimeout * 2,
+              'grpc.keepalive_timeout_ms': grpcAgentKeepAliveTimeout,
+            },
+          ),
+        )
+        container
+          .bind<ServiceProxyInterface>(TYPES.ApiGateway_ServiceProxy)
+          .toConstantValue(
+            new GRPCServiceProxy(
+              container.get<AxiosInstance>(TYPES.ApiGateway_HTTPClient),
+              container.get<string>(TYPES.ApiGateway_AUTH_SERVER_URL),
+              container.get<string>(TYPES.ApiGateway_SYNCING_SERVER_JS_URL),
+              container.get<string>(TYPES.ApiGateway_PAYMENTS_SERVER_URL),
+              container.get<string>(TYPES.ApiGateway_FILES_SERVER_URL),
+              container.get<string>(TYPES.ApiGateway_WEB_SOCKET_SERVER_URL),
+              container.get<string>(TYPES.ApiGateway_REVISIONS_SERVER_URL),
+              container.get<string>(TYPES.ApiGateway_EMAIL_SERVER_URL),
+              container.get<number>(TYPES.ApiGateway_HTTP_CALL_TIMEOUT),
+              container.get<CrossServiceTokenCacheInterface>(TYPES.ApiGateway_CrossServiceTokenCache),
+              container.get<winston.Logger>(TYPES.ApiGateway_Logger),
+              container.get<TimerInterface>(TYPES.ApiGateway_Timer),
+              container.get<ISessionsClient>(TYPES.ApiGateway_GRPCSessionsClient),
+            ),
+          )
+      } else {
+        container.bind<ServiceProxyInterface>(TYPES.ApiGateway_ServiceProxy).to(HttpServiceProxy)
+      }
     }
 
     if (isConfiguredForHomeServer) {

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

@@ -5,6 +5,7 @@ export const TYPES = {
   // env vars
   ApiGateway_SYNCING_SERVER_JS_URL: Symbol.for('ApiGateway_SYNCING_SERVER_JS_URL'),
   ApiGateway_AUTH_SERVER_URL: Symbol.for('ApiGateway_AUTH_SERVER_URL'),
+  ApiGateway_AUTH_SERVER_GRPC_URL: Symbol.for('ApiGateway_AUTH_SERVER_GRPC_URL'),
   ApiGateway_PAYMENTS_SERVER_URL: Symbol.for('ApiGateway_PAYMENTS_SERVER_URL'),
   ApiGateway_FILES_SERVER_URL: Symbol.for('ApiGateway_FILES_SERVER_URL'),
   ApiGateway_REVISIONS_SERVER_URL: Symbol.for('ApiGateway_REVISIONS_SERVER_URL'),
@@ -28,4 +29,5 @@ export const TYPES = {
   ApiGateway_CrossServiceTokenCache: Symbol.for('ApiGateway_CrossServiceTokenCache'),
   ApiGateway_Timer: Symbol.for('ApiGateway_Timer'),
   ApiGateway_EndpointResolver: Symbol.for('ApiGateway_EndpointResolver'),
+  ApiGateway_GRPCSessionsClient: Symbol.for('ApiGateway_GRPCSessionsClient'),
 }

+ 1 - 1
packages/api-gateway/src/Controller/AuthMiddleware.ts

@@ -7,7 +7,7 @@ import { AxiosError } from 'axios'
 import { Logger } from 'winston'
 
 import { CrossServiceTokenCacheInterface } from '../Service/Cache/CrossServiceTokenCacheInterface'
-import { ServiceProxyInterface } from '../Service/Http/ServiceProxyInterface'
+import { ServiceProxyInterface } from '../Service/Proxy/ServiceProxyInterface'
 
 export abstract class AuthMiddleware extends BaseMiddleware {
   constructor(

+ 1 - 1
packages/api-gateway/src/Controller/LegacyController.ts

@@ -2,7 +2,7 @@ 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 { ServiceProxyInterface } from '../Service/Http/ServiceProxyInterface'
+import { ServiceProxyInterface } from '../Service/Proxy/ServiceProxyInterface'
 
 @controller('')
 export class LegacyController extends BaseHttpController {

+ 1 - 1
packages/api-gateway/src/Controller/OptionalCrossServiceTokenMiddleware.ts

@@ -5,7 +5,7 @@ import { Logger } from 'winston'
 
 import { TYPES } from '../Bootstrap/Types'
 import { CrossServiceTokenCacheInterface } from '../Service/Cache/CrossServiceTokenCacheInterface'
-import { ServiceProxyInterface } from '../Service/Http/ServiceProxyInterface'
+import { ServiceProxyInterface } from '../Service/Proxy/ServiceProxyInterface'
 import { AuthMiddleware } from './AuthMiddleware'
 
 @injectable()

+ 1 - 1
packages/api-gateway/src/Controller/RequiredCrossServiceTokenMiddleware.ts

@@ -5,7 +5,7 @@ import { Logger } from 'winston'
 
 import { TYPES } from '../Bootstrap/Types'
 import { CrossServiceTokenCacheInterface } from '../Service/Cache/CrossServiceTokenCacheInterface'
-import { ServiceProxyInterface } from '../Service/Http/ServiceProxyInterface'
+import { ServiceProxyInterface } from '../Service/Proxy/ServiceProxyInterface'
 import { AuthMiddleware } from './AuthMiddleware'
 
 @injectable()

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

@@ -2,7 +2,7 @@ import { Request, Response } from 'express'
 import { inject } from 'inversify'
 import { BaseHttpController, controller, httpGet, httpPost } from 'inversify-express-utils'
 import { TYPES } from '../../Bootstrap/Types'
-import { ServiceProxyInterface } from '../../Service/Http/ServiceProxyInterface'
+import { ServiceProxyInterface } from '../../Service/Proxy/ServiceProxyInterface'
 import { EndpointResolverInterface } from '../../Service/Resolver/EndpointResolverInterface'
 
 @controller('/v1')

+ 1 - 1
packages/api-gateway/src/Controller/v1/AuthenticatorsController.ts

@@ -3,7 +3,7 @@ import { Request, Response } from 'express'
 import { controller, BaseHttpController, httpPost, httpGet, httpDelete } from 'inversify-express-utils'
 
 import { TYPES } from '../../Bootstrap/Types'
-import { ServiceProxyInterface } from '../../Service/Http/ServiceProxyInterface'
+import { ServiceProxyInterface } from '../../Service/Proxy/ServiceProxyInterface'
 import { EndpointResolverInterface } from '../../Service/Resolver/EndpointResolverInterface'
 
 @controller('/v1/authenticators')

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

@@ -3,7 +3,7 @@ import { inject } from 'inversify'
 import { BaseHttpController, controller, httpPost } from 'inversify-express-utils'
 
 import { TYPES } from '../../Bootstrap/Types'
-import { ServiceProxyInterface } from '../../Service/Http/ServiceProxyInterface'
+import { ServiceProxyInterface } from '../../Service/Proxy/ServiceProxyInterface'
 import { EndpointResolverInterface } from '../../Service/Resolver/EndpointResolverInterface'
 
 @controller('/v1/files')

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

@@ -2,7 +2,7 @@ import { Request, Response } from 'express'
 import { BaseHttpController, controller, httpPost } from 'inversify-express-utils'
 import { inject } from 'inversify'
 import { TYPES } from '../../Bootstrap/Types'
-import { ServiceProxyInterface } from '../../Service/Http/ServiceProxyInterface'
+import { ServiceProxyInterface } from '../../Service/Proxy/ServiceProxyInterface'
 
 @controller('/v1')
 export class InvoicesController extends BaseHttpController {

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

@@ -2,7 +2,7 @@ import { Request, Response } from 'express'
 import { inject } from 'inversify'
 import { BaseHttpController, controller, httpGet, httpPost } from 'inversify-express-utils'
 import { TYPES } from '../../Bootstrap/Types'
-import { ServiceProxyInterface } from '../../Service/Http/ServiceProxyInterface'
+import { ServiceProxyInterface } from '../../Service/Proxy/ServiceProxyInterface'
 import { EndpointResolverInterface } from '../../Service/Resolver/EndpointResolverInterface'
 
 @controller('/v1/items', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)

+ 1 - 1
packages/api-gateway/src/Controller/v1/MessagesController.ts

@@ -2,7 +2,7 @@ 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 { ServiceProxyInterface } from '../../Service/Http/ServiceProxyInterface'
+import { ServiceProxyInterface } from '../../Service/Proxy/ServiceProxyInterface'
 import { EndpointResolverInterface } from '../../Service/Resolver/EndpointResolverInterface'
 
 @controller('/v1/messages', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)

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

@@ -3,7 +3,7 @@ import { inject } from 'inversify'
 import { BaseHttpController, controller, httpGet, httpPost } from 'inversify-express-utils'
 
 import { TYPES } from '../../Bootstrap/Types'
-import { ServiceProxyInterface } from '../../Service/Http/ServiceProxyInterface'
+import { ServiceProxyInterface } from '../../Service/Proxy/ServiceProxyInterface'
 import { EndpointResolverInterface } from '../../Service/Resolver/EndpointResolverInterface'
 
 @controller('/v1/offline')

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

@@ -2,7 +2,7 @@ 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 { ServiceProxyInterface } from '../../Service/Http/ServiceProxyInterface'
+import { ServiceProxyInterface } from '../../Service/Proxy/ServiceProxyInterface'
 
 @controller('/v1')
 export class PaymentsController extends BaseHttpController {

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

@@ -2,7 +2,7 @@ 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 { ServiceProxyInterface } from '../../Service/Http/ServiceProxyInterface'
+import { ServiceProxyInterface } from '../../Service/Proxy/ServiceProxyInterface'
 import { EndpointResolverInterface } from '../../Service/Resolver/EndpointResolverInterface'
 
 @controller('/v1/sessions')

+ 1 - 1
packages/api-gateway/src/Controller/v1/SharedVaultInvitesController.ts

@@ -2,7 +2,7 @@ import { Request, Response } from 'express'
 import { inject } from 'inversify'
 import { BaseHttpController, controller, httpDelete, httpGet, httpPatch, httpPost } from 'inversify-express-utils'
 import { TYPES } from '../../Bootstrap/Types'
-import { ServiceProxyInterface } from '../../Service/Http/ServiceProxyInterface'
+import { ServiceProxyInterface } from '../../Service/Proxy/ServiceProxyInterface'
 import { EndpointResolverInterface } from '../../Service/Resolver/EndpointResolverInterface'
 
 @controller('/v1/shared-vaults', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)

+ 1 - 1
packages/api-gateway/src/Controller/v1/SharedVaultUsersController.ts

@@ -2,7 +2,7 @@ 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 { ServiceProxyInterface } from '../../Service/Http/ServiceProxyInterface'
+import { ServiceProxyInterface } from '../../Service/Proxy/ServiceProxyInterface'
 import { EndpointResolverInterface } from '../../Service/Resolver/EndpointResolverInterface'
 
 @controller('/v1/shared-vaults/:sharedVaultUuid/users', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)

+ 1 - 1
packages/api-gateway/src/Controller/v1/SharedVaultsController.ts

@@ -2,7 +2,7 @@ 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 { ServiceProxyInterface } from '../../Service/Http/ServiceProxyInterface'
+import { ServiceProxyInterface } from '../../Service/Proxy/ServiceProxyInterface'
 import { EndpointResolverInterface } from '../../Service/Resolver/EndpointResolverInterface'
 
 @controller('/v1/shared-vaults', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)

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

@@ -3,7 +3,7 @@ import { inject } from 'inversify'
 import { BaseHttpController, controller, httpDelete, httpGet, httpPost } from 'inversify-express-utils'
 
 import { TYPES } from '../../Bootstrap/Types'
-import { ServiceProxyInterface } from '../../Service/Http/ServiceProxyInterface'
+import { ServiceProxyInterface } from '../../Service/Proxy/ServiceProxyInterface'
 import { EndpointResolverInterface } from '../../Service/Resolver/EndpointResolverInterface'
 
 @controller('/v1/subscription-invites')

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

@@ -3,7 +3,7 @@ import { inject } from 'inversify'
 import { BaseHttpController, controller, httpPost } from 'inversify-express-utils'
 
 import { TYPES } from '../../Bootstrap/Types'
-import { ServiceProxyInterface } from '../../Service/Http/ServiceProxyInterface'
+import { ServiceProxyInterface } from '../../Service/Proxy/ServiceProxyInterface'
 import { EndpointResolverInterface } from '../../Service/Resolver/EndpointResolverInterface'
 
 @controller('/v1/subscription-tokens')

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

@@ -13,7 +13,7 @@ import {
 } from 'inversify-express-utils'
 import { Logger } from 'winston'
 import { TYPES } from '../../Bootstrap/Types'
-import { ServiceProxyInterface } from '../../Service/Http/ServiceProxyInterface'
+import { ServiceProxyInterface } from '../../Service/Proxy/ServiceProxyInterface'
 import { TokenAuthenticationMethod } from '../TokenAuthenticationMethod'
 import { EndpointResolverInterface } from '../../Service/Resolver/EndpointResolverInterface'
 

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

@@ -4,7 +4,7 @@ import { BaseHttpController, controller, httpDelete, httpPost } from 'inversify-
 import { Logger } from 'winston'
 
 import { TYPES } from '../../Bootstrap/Types'
-import { ServiceProxyInterface } from '../../Service/Http/ServiceProxyInterface'
+import { ServiceProxyInterface } from '../../Service/Proxy/ServiceProxyInterface'
 import { EndpointResolverInterface } from '../../Service/Resolver/EndpointResolverInterface'
 
 @controller('/v1/sockets')

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

@@ -3,7 +3,7 @@ import { inject } from 'inversify'
 import { BaseHttpController, controller, httpPost } from 'inversify-express-utils'
 
 import { TYPES } from '../../Bootstrap/Types'
-import { ServiceProxyInterface } from '../../Service/Http/ServiceProxyInterface'
+import { ServiceProxyInterface } from '../../Service/Proxy/ServiceProxyInterface'
 import { EndpointResolverInterface } from '../../Service/Resolver/EndpointResolverInterface'
 
 @controller('/v2')

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

@@ -2,7 +2,7 @@ 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 { ServiceProxyInterface } from '../../Service/Http/ServiceProxyInterface'
+import { ServiceProxyInterface } from '../../Service/Proxy/ServiceProxyInterface'
 
 @controller('/v2')
 export class PaymentsControllerV2 extends BaseHttpController {

+ 1 - 1
packages/api-gateway/src/Controller/v2/RevisionsControllerV2.ts

@@ -3,7 +3,7 @@ import { inject } from 'inversify'
 import { BaseHttpController, controller, httpDelete, httpGet } from 'inversify-express-utils'
 
 import { TYPES } from '../../Bootstrap/Types'
-import { ServiceProxyInterface } from '../../Service/Http/ServiceProxyInterface'
+import { ServiceProxyInterface } from '../../Service/Proxy/ServiceProxyInterface'
 import { EndpointResolverInterface } from '../../Service/Resolver/EndpointResolverInterface'
 
 @controller('/v2', TYPES.ApiGateway_RequiredCrossServiceTokenMiddleware)

+ 1 - 1
packages/api-gateway/src/Service/Proxy/DirectCallServiceProxy.ts → packages/api-gateway/src/Service/DirectCall/DirectCallServiceProxy.ts

@@ -1,7 +1,7 @@
 import { Request, Response } from 'express'
 import { ServiceContainerInterface, ServiceIdentifier } from '@standardnotes/domain-core'
 
-import { ServiceProxyInterface } from '../Http/ServiceProxyInterface'
+import { ServiceProxyInterface } from '../Proxy/ServiceProxyInterface'
 
 export class DirectCallServiceProxy implements ServiceProxyInterface {
   constructor(

+ 1 - 1
packages/api-gateway/src/Service/Http/HttpServiceProxy.ts

@@ -6,7 +6,7 @@ import { Logger } from 'winston'
 
 import { TYPES } from '../../Bootstrap/Types'
 import { CrossServiceTokenCacheInterface } from '../Cache/CrossServiceTokenCacheInterface'
-import { ServiceProxyInterface } from './ServiceProxyInterface'
+import { ServiceProxyInterface } from '../Proxy/ServiceProxyInterface'
 import { TimerInterface } from '@standardnotes/time'
 
 @injectable()

+ 0 - 0
packages/api-gateway/src/Service/Http/ServiceProxyInterface.ts → packages/api-gateway/src/Service/Proxy/ServiceProxyInterface.ts


+ 440 - 0
packages/api-gateway/src/Service/gRPC/GRPCServiceProxy.ts

@@ -0,0 +1,440 @@
+import { AxiosInstance, AxiosError, AxiosResponse, Method } from 'axios'
+import { Request, Response } from 'express'
+import { Logger } from 'winston'
+import { TimerInterface } from '@standardnotes/time'
+import { ISessionsClient, AuthorizationHeader, SessionValidationResponse } from '@standardnotes/grpc'
+import * as grpc from '@grpc/grpc-js'
+
+import { CrossServiceTokenCacheInterface } from '../Cache/CrossServiceTokenCacheInterface'
+import { ServiceProxyInterface } from '../Proxy/ServiceProxyInterface'
+
+export class GRPCServiceProxy implements ServiceProxyInterface {
+  constructor(
+    private httpClient: AxiosInstance,
+    private authServerUrl: string,
+    private syncingServerJsUrl: string,
+    private paymentsServerUrl: string,
+    private filesServerUrl: string,
+    private webSocketServerUrl: string,
+    private revisionsServerUrl: string,
+    private emailServerUrl: string,
+    private httpCallTimeout: number,
+    private crossServiceTokenCache: CrossServiceTokenCacheInterface,
+    private logger: Logger,
+    private timer: TimerInterface,
+    private sessionsClient: ISessionsClient,
+  ) {}
+
+  async validateSession(headers: {
+    authorization: string
+    sharedVaultOwnerContext?: string
+  }): Promise<{ status: number; data: unknown; headers: { contentType: string } }> {
+    return new Promise((resolve, reject) => {
+      try {
+        const request = new AuthorizationHeader()
+        request.setBearerToken(headers.authorization)
+
+        const metadata = new grpc.Metadata()
+        metadata.set('x-shared-vault-owner-context', headers.sharedVaultOwnerContext ?? '')
+
+        this.sessionsClient.validate(
+          request,
+          metadata,
+          (error: grpc.ServiceError | null, response: SessionValidationResponse) => {
+            if (error) {
+              const responseCode = error.metadata.get('x-auth-error-response-code').pop()
+              if (responseCode) {
+                return resolve({
+                  status: +responseCode,
+                  data: {
+                    error: {
+                      message: error.metadata.get('x-auth-error-message').pop(),
+                      tag: error.metadata.get('x-auth-error-tag').pop(),
+                    },
+                  },
+                  headers: {
+                    contentType: 'application/json',
+                  },
+                })
+              }
+
+              return reject(error)
+            }
+
+            return resolve({
+              status: 200,
+              data: {
+                authToken: response.getCrossServiceToken(),
+              },
+              headers: {
+                contentType: 'application/json',
+              },
+            })
+          },
+        )
+      } catch (error) {
+        return reject(error)
+      }
+    })
+  }
+
+  async callSyncingServer(
+    request: Request,
+    response: Response,
+    endpointOrMethodIdentifier: string,
+    payload?: Record<string, unknown> | string,
+  ): Promise<void> {
+    await this.callServer(this.syncingServerJsUrl, request, response, endpointOrMethodIdentifier, payload)
+  }
+
+  async callRevisionsServer(
+    request: Request,
+    response: Response,
+    endpointOrMethodIdentifier: string,
+    payload?: Record<string, unknown> | string,
+  ): Promise<void> {
+    if (!this.revisionsServerUrl) {
+      response.status(400).send({ message: 'Revisions Server not configured' })
+
+      return
+    }
+    await this.callServer(this.revisionsServerUrl, request, response, endpointOrMethodIdentifier, payload)
+  }
+
+  async callLegacySyncingServer(
+    request: Request,
+    response: Response,
+    endpointOrMethodIdentifier: string,
+    payload?: Record<string, unknown> | string,
+  ): Promise<void> {
+    await this.callServerWithLegacyFormat(
+      this.syncingServerJsUrl,
+      request,
+      response,
+      endpointOrMethodIdentifier,
+      payload,
+    )
+  }
+
+  async callAuthServer(
+    request: Request,
+    response: Response,
+    endpointOrMethodIdentifier: string,
+    payload?: Record<string, unknown> | string,
+  ): Promise<void> {
+    await this.callServer(this.authServerUrl, request, response, endpointOrMethodIdentifier, payload)
+  }
+
+  async callEmailServer(
+    request: Request,
+    response: Response,
+    endpointOrMethodIdentifier: string,
+    payload?: Record<string, unknown> | string,
+  ): Promise<void> {
+    if (!this.emailServerUrl) {
+      response.status(400).send({ message: 'Email Server not configured' })
+
+      return
+    }
+
+    await this.callServer(this.emailServerUrl, request, response, endpointOrMethodIdentifier, payload)
+  }
+
+  async callWebSocketServer(
+    request: Request,
+    response: Response,
+    endpointOrMethodIdentifier: string,
+    payload?: Record<string, unknown> | string,
+  ): Promise<void> {
+    if (!this.webSocketServerUrl) {
+      this.logger.debug('Websockets Server URL not defined. Skipped request to WebSockets API.')
+
+      return
+    }
+
+    const isARequestComingFromApiGatewayAndShouldBeKeptInMinimalFormat = request.headers.connectionid !== undefined
+    this.logger.debug(
+      `Calling websockets service: ${endpointOrMethodIdentifier}. Format is minimal: ${isARequestComingFromApiGatewayAndShouldBeKeptInMinimalFormat}`,
+    )
+    if (isARequestComingFromApiGatewayAndShouldBeKeptInMinimalFormat) {
+      await this.callServerWithLegacyFormat(
+        this.webSocketServerUrl,
+        request,
+        response,
+        endpointOrMethodIdentifier,
+        payload,
+      )
+    } else {
+      await this.callServer(this.webSocketServerUrl, request, response, endpointOrMethodIdentifier, payload)
+    }
+  }
+
+  async callPaymentsServer(
+    request: Request,
+    response: Response,
+    endpointOrMethodIdentifier: string,
+    payload?: Record<string, unknown> | string,
+  ): Promise<void | Response<unknown, Record<string, unknown>>> {
+    if (!this.paymentsServerUrl) {
+      this.logger.debug('Payments Server URL not defined. Skipped request to Payments API.')
+
+      return
+    }
+
+    await this.callServerWithLegacyFormat(
+      this.paymentsServerUrl,
+      request,
+      response,
+      endpointOrMethodIdentifier,
+      payload,
+    )
+  }
+
+  async callAuthServerWithLegacyFormat(
+    request: Request,
+    response: Response,
+    endpointOrMethodIdentifier: string,
+    payload?: Record<string, unknown> | string,
+  ): Promise<void> {
+    await this.callServerWithLegacyFormat(this.authServerUrl, request, response, endpointOrMethodIdentifier, payload)
+  }
+
+  private async getServerResponse(
+    serverUrl: string,
+    request: Request,
+    response: Response,
+    endpointOrMethodIdentifier: string,
+    payload?: Record<string, unknown> | string,
+    retryAttempt?: number,
+  ): 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}/${endpointOrMethodIdentifier},
+        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}/${endpointOrMethodIdentifier}`,
+        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)
+      }
+
+      if (retryAttempt) {
+        this.logger.debug(
+          `Request to ${serverUrl}/${endpointOrMethodIdentifier} succeeded after ${retryAttempt} retries`,
+        )
+      }
+
+      return serviceResponse
+    } catch (error) {
+      const requestDidNotMakeIt = this.requestTimedOutOrDidNotReachDestination(error as Record<string, unknown>)
+      const tooManyRetryAttempts = retryAttempt && retryAttempt > 2
+      if (!tooManyRetryAttempts && requestDidNotMakeIt) {
+        await this.timer.sleep(50)
+
+        const nextRetryAttempt = retryAttempt ? retryAttempt + 1 : 1
+
+        this.logger.debug(
+          `Retrying request to ${serverUrl}/${endpointOrMethodIdentifier} for the ${nextRetryAttempt} time`,
+        )
+
+        return this.getServerResponse(
+          serverUrl,
+          request,
+          response,
+          endpointOrMethodIdentifier,
+          payload,
+          nextRetryAttempt,
+        )
+      }
+
+      let detailedErrorMessage = (error as Error).message
+      if (error instanceof AxiosError) {
+        detailedErrorMessage = `Status: ${error.status}, code: ${error.code}, message: ${error.message}`
+      }
+
+      this.logger.error(
+        tooManyRetryAttempts
+          ? `Request to ${serverUrl}/${endpointOrMethodIdentifier} timed out after ${retryAttempt} retries`
+          : `Could not pass the request to ${serverUrl}/${endpointOrMethodIdentifier} on underlying service: ${detailedErrorMessage}`,
+      )
+
+      this.logger.debug(`Response error: ${JSON.stringify(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 && !isNaN(+((error as AxiosError).code as string))
+          ? +((error as AxiosError).code as string)
+          : 500
+
+      const responseErrorMessage = (error as AxiosError).response?.data
+
+      response
+        .status(errorCode)
+        .send(
+          responseErrorMessage ??
+            "Unfortunately, we couldn't handle your request. Please try again or contact our support if the error persists.",
+        )
+    }
+
+    return
+  }
+
+  private async callServer(
+    serverUrl: string,
+    request: Request,
+    response: Response,
+    endpointOrMethodIdentifier: string,
+    payload?: Record<string, unknown> | string,
+  ): Promise<void> {
+    const serviceResponse = await this.getServerResponse(
+      serverUrl,
+      request,
+      response,
+      endpointOrMethodIdentifier,
+      payload,
+    )
+
+    this.logger.debug(`Response from underlying server: ${JSON.stringify(serviceResponse?.data)},
+      headers: ${JSON.stringify(serviceResponse?.headers)}`)
+
+    if (!serviceResponse) {
+      return
+    }
+
+    this.applyResponseHeaders(serviceResponse, response)
+
+    if (this.responseShouldNotBeDecorated(serviceResponse)) {
+      response.status(serviceResponse.status).send(serviceResponse.data)
+
+      return
+    }
+
+    response.status(serviceResponse.status).send({
+      meta: {
+        auth: {
+          userUuid: response.locals.user?.uuid,
+          roles: response.locals.roles,
+        },
+        server: {
+          filesServerUrl: this.filesServerUrl,
+        },
+      },
+      data: serviceResponse.data,
+    })
+  }
+
+  private async callServerWithLegacyFormat(
+    serverUrl: string,
+    request: Request,
+    response: Response,
+    endpointOrMethodIdentifier: string,
+    payload?: Record<string, unknown> | string,
+  ): Promise<void | Response<unknown, Record<string, unknown>>> {
+    const serviceResponse = await this.getServerResponse(
+      serverUrl,
+      request,
+      response,
+      endpointOrMethodIdentifier,
+      payload,
+    )
+
+    if (!serviceResponse) {
+      return
+    }
+
+    this.applyResponseHeaders(serviceResponse, response)
+
+    if (serviceResponse.request._redirectable._redirectCount > 0) {
+      response.status(302)
+
+      response.redirect(serviceResponse.request.res.responseUrl)
+    } else {
+      response.status(serviceResponse.status)
+
+      response.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 responseShouldNotBeDecorated(serviceResponse: AxiosResponse): boolean {
+    return (
+      serviceResponse.headers['content-type'] !== undefined &&
+      serviceResponse.headers['content-type'].toLowerCase().includes('text/html')
+    )
+  }
+
+  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)
+      }
+    })
+  }
+
+  private requestTimedOutOrDidNotReachDestination(error: Record<string, unknown>): boolean {
+    return (
+      ('code' in error && error.code === 'ETIMEDOUT') ||
+      ('response' in error &&
+        'status' in (error.response as Record<string, unknown>) &&
+        [503, 504].includes((error.response as Record<string, unknown>).status as number))
+    )
+  }
+}

+ 1 - 0
packages/auth/.env.sample

@@ -13,6 +13,7 @@ AUTH_JWT_TTL=60000
 ENCRYPTION_SERVER_KEY=change-me-!
 
 PORT=3000
+GRPC_PORT=50051
 
 DB_HOST=127.0.0.1
 DB_REPLICA_HOST=127.0.0.1

+ 40 - 2
packages/auth/bin/server.ts

@@ -20,6 +20,7 @@ import '../src/Infra/InversifyExpressUtils/AnnotatedHealthCheckController'
 import '../src/Infra/InversifyExpressUtils/AnnotatedFeaturesController'
 
 import * as cors from 'cors'
+import * as grpc from '@grpc/grpc-js'
 import { urlencoded, json, Request, Response, NextFunction } from 'express'
 import * as winston from 'winston'
 import * as dayjs from 'dayjs'
@@ -29,6 +30,10 @@ import { InversifyExpressServer } from 'inversify-express-utils'
 import { ContainerConfigLoader } from '../src/Bootstrap/Container'
 import TYPES from '../src/Bootstrap/Types'
 import { Env } from '../src/Bootstrap/Env'
+import { SessionsServer } from '../src/Infra/gRPC/SessionsServer'
+import { SessionsService } from '@standardnotes/grpc'
+import { AuthenticateRequest } from '../src/Domain/UseCase/AuthenticateRequest'
+import { CreateCrossServiceToken } from '../src/Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken'
 
 const container = new ContainerConfigLoader()
 void container.load().then((container) => {
@@ -66,9 +71,42 @@ void container.load().then((container) => {
 
   const serverInstance = server.build().listen(env.get('PORT'))
 
-  const keepAliveTimeout = env.get('KEEP_ALIVE_TIMEOUT', true) ? +env.get('KEEP_ALIVE_TIMEOUT', true) : 5000
+  const httpKeepAliveTimeout = env.get('HTTP_KEEP_ALIVE_TIMEOUT', true)
+    ? +env.get('HTTP_KEEP_ALIVE_TIMEOUT', true)
+    : 10_000
 
-  serverInstance.keepAliveTimeout = keepAliveTimeout
+  serverInstance.keepAliveTimeout = httpKeepAliveTimeout
+
+  const grpcKeepAliveTimeout = env.get('GRPC_KEEP_ALIVE_TIMEOUT', true)
+    ? +env.get('GRPC_KEEP_ALIVE_TIMEOUT', true)
+    : 10_000
+
+  const grpcServer = new grpc.Server({
+    'grpc.keepalive_time_ms': grpcKeepAliveTimeout * 2,
+    'grpc.keepalive_timeout_ms': grpcKeepAliveTimeout,
+  })
+
+  const gRPCPort = env.get('GRPC_PORT', true) ? +env.get('GRPC_PORT', true) : 50051
+
+  const sessionsServer = new SessionsServer(
+    container.get<AuthenticateRequest>(TYPES.Auth_AuthenticateRequest),
+    container.get<CreateCrossServiceToken>(TYPES.Auth_CreateCrossServiceToken),
+  )
+
+  grpcServer.addService(SessionsService, {
+    validate: sessionsServer.validate.bind(sessionsServer),
+  })
+  grpcServer.bindAsync(`0.0.0.0:${gRPCPort}`, grpc.ServerCredentials.createInsecure(), (error, port) => {
+    if (error) {
+      logger.error(`Failed to bind gRPC server: ${error.message}`)
+    }
+
+    logger.info(`gRPC server bound on port ${port}`)
+
+    grpcServer.start()
+
+    logger.info('gRPC server started')
+  })
 
   process.on('SIGTERM', () => {
     logger.info('SIGTERM signal received: closing HTTP server')

+ 3 - 0
packages/auth/package.json

@@ -20,6 +20,7 @@
     "lint:fix": "eslint . --fix --ext .ts",
     "pretest": "yarn lint && yarn build",
     "test": "jest --coverage --no-cache --config=./jest.config.js --maxWorkers=50%",
+    "grpc": "yarn node dist/bin/grpc.js",
     "start": "yarn node dist/bin/server.js",
     "worker": "yarn node dist/bin/worker.js",
     "cleanup": "yarn node dist/bin/cleanup.js",
@@ -37,6 +38,7 @@
     "@aws-sdk/client-sqs": "^3.427.0",
     "@cbor-extract/cbor-extract-linux-arm64": "^2.1.1",
     "@cbor-extract/cbor-extract-linux-x64": "^2.1.1",
+    "@grpc/grpc-js": "^1.9.10",
     "@simplewebauthn/server": "^8.1.1",
     "@simplewebauthn/typescript-types": "^8.0.0",
     "@standardnotes/api": "^1.26.26",
@@ -45,6 +47,7 @@
     "@standardnotes/domain-events": "workspace:*",
     "@standardnotes/domain-events-infra": "workspace:*",
     "@standardnotes/features": "^1.59.7",
+    "@standardnotes/grpc": "workspace:^",
     "@standardnotes/predicates": "workspace:*",
     "@standardnotes/responses": "^1.13.27",
     "@standardnotes/security": "workspace:*",

+ 74 - 0
packages/auth/src/Infra/gRPC/SessionsServer.ts

@@ -0,0 +1,74 @@
+import * as grpc from '@grpc/grpc-js'
+import { Status } from '@grpc/grpc-js/build/src/constants'
+
+import { AuthorizationHeader, ISessionsServer, SessionValidationResponse } from '@standardnotes/grpc'
+
+import { AuthenticateRequest } from '../../Domain/UseCase/AuthenticateRequest'
+import { User } from '../../Domain/User/User'
+import { CreateCrossServiceToken } from '../../Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken'
+
+export class SessionsServer implements ISessionsServer {
+  constructor(
+    private authenticateRequest: AuthenticateRequest,
+    private createCrossServiceToken: CreateCrossServiceToken,
+  ) {}
+
+  async validate(
+    call: grpc.ServerUnaryCall<AuthorizationHeader, SessionValidationResponse>,
+    callback: grpc.sendUnaryData<SessionValidationResponse>,
+  ): Promise<void> {
+    const authenticateRequestResponse = await this.authenticateRequest.execute({
+      authorizationHeader: call.request.getBearerToken(),
+    })
+
+    if (!authenticateRequestResponse.success) {
+      const metadata = new grpc.Metadata()
+      metadata.set('x-auth-error-message', authenticateRequestResponse.errorMessage as string)
+      metadata.set('x-auth-error-tag', authenticateRequestResponse.errorTag as string)
+      metadata.set('x-auth-error-response-code', authenticateRequestResponse.responseCode.toString())
+      return callback(
+        {
+          code: Status.PERMISSION_DENIED,
+          message: authenticateRequestResponse.errorMessage,
+          name: authenticateRequestResponse.errorTag,
+          metadata,
+        },
+        null,
+      )
+    }
+
+    const user = authenticateRequestResponse.user as User
+
+    const sharedVaultOwnerMetadata = call.metadata.get('x-shared-vault-owner-context')
+    let sharedVaultOwnerContext = undefined
+    if (sharedVaultOwnerMetadata.length > 0 && sharedVaultOwnerMetadata[0].length > 0) {
+      sharedVaultOwnerContext = sharedVaultOwnerMetadata[0].toString()
+    }
+
+    const resultOrError = await this.createCrossServiceToken.execute({
+      user,
+      session: authenticateRequestResponse.session,
+      sharedVaultOwnerContext,
+    })
+    if (resultOrError.isFailed()) {
+      const metadata = new grpc.Metadata()
+      metadata.set('x-auth-error-message', resultOrError.getError())
+      metadata.set('x-auth-error-response-code', '400')
+
+      return callback(
+        {
+          code: Status.INVALID_ARGUMENT,
+          message: resultOrError.getError(),
+          name: 'INVALID_ARGUMENT',
+          metadata,
+        },
+        null,
+      )
+    }
+
+    const response = new SessionValidationResponse()
+    response.setCrossServiceToken(resultOrError.getValue())
+
+    callback(null, response)
+  }
+}

+ 1 - 1
packages/files/bin/server.ts

@@ -91,7 +91,7 @@ void container.load().then((container) => {
 
   const serverInstance = server.build().listen(env.get('PORT'))
 
-  const keepAliveTimeout = env.get('KEEP_ALIVE_TIMEOUT', true) ? +env.get('KEEP_ALIVE_TIMEOUT', true) : 5000
+  const keepAliveTimeout = env.get('HTTP_KEEP_ALIVE_TIMEOUT', true) ? +env.get('HTTP_KEEP_ALIVE_TIMEOUT', true) : 5000
 
   serverInstance.keepAliveTimeout = keepAliveTimeout
 

+ 41 - 0
packages/grpc/lib/auth_grpc_pb.d.ts

@@ -0,0 +1,41 @@
+// package: auth
+// file: auth.proto
+
+/* tslint:disable */
+/* eslint-disable */
+
+import * as grpc from "@grpc/grpc-js";
+import * as auth_pb from "./auth_pb";
+
+interface ISessionsService extends grpc.ServiceDefinition<grpc.UntypedServiceImplementation> {
+    validate: ISessionsService_Ivalidate;
+}
+
+interface ISessionsService_Ivalidate extends grpc.MethodDefinition<auth_pb.AuthorizationHeader, auth_pb.SessionValidationResponse> {
+    path: "/auth.Sessions/validate";
+    requestStream: false;
+    responseStream: false;
+    requestSerialize: grpc.serialize<auth_pb.AuthorizationHeader>;
+    requestDeserialize: grpc.deserialize<auth_pb.AuthorizationHeader>;
+    responseSerialize: grpc.serialize<auth_pb.SessionValidationResponse>;
+    responseDeserialize: grpc.deserialize<auth_pb.SessionValidationResponse>;
+}
+
+export const SessionsService: ISessionsService;
+
+export interface ISessionsServer {
+    validate: grpc.handleUnaryCall<auth_pb.AuthorizationHeader, auth_pb.SessionValidationResponse>;
+}
+
+export interface ISessionsClient {
+    validate(request: auth_pb.AuthorizationHeader, callback: (error: grpc.ServiceError | null, response: auth_pb.SessionValidationResponse) => void): grpc.ClientUnaryCall;
+    validate(request: auth_pb.AuthorizationHeader, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: auth_pb.SessionValidationResponse) => void): grpc.ClientUnaryCall;
+    validate(request: auth_pb.AuthorizationHeader, metadata: grpc.Metadata, options: Partial<grpc.CallOptions>, callback: (error: grpc.ServiceError | null, response: auth_pb.SessionValidationResponse) => void): grpc.ClientUnaryCall;
+}
+
+export class SessionsClient extends grpc.Client implements ISessionsClient {
+    constructor(address: string, credentials: grpc.ChannelCredentials, options?: object);
+    public validate(request: auth_pb.AuthorizationHeader, callback: (error: grpc.ServiceError | null, response: auth_pb.SessionValidationResponse) => void): grpc.ClientUnaryCall;
+    public validate(request: auth_pb.AuthorizationHeader, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: auth_pb.SessionValidationResponse) => void): grpc.ClientUnaryCall;
+    public validate(request: auth_pb.AuthorizationHeader, metadata: grpc.Metadata, options: Partial<grpc.CallOptions>, callback: (error: grpc.ServiceError | null, response: auth_pb.SessionValidationResponse) => void): grpc.ClientUnaryCall;
+}

+ 44 - 0
packages/grpc/lib/auth_grpc_pb.js

@@ -0,0 +1,44 @@
+// GENERATED CODE -- DO NOT EDIT!
+
+'use strict';
+var grpc = require('@grpc/grpc-js');
+var auth_pb = require('./auth_pb.js');
+
+function serialize_auth_AuthorizationHeader(arg) {
+  if (!(arg instanceof auth_pb.AuthorizationHeader)) {
+    throw new Error('Expected argument of type auth.AuthorizationHeader');
+  }
+  return Buffer.from(arg.serializeBinary());
+}
+
+function deserialize_auth_AuthorizationHeader(buffer_arg) {
+  return auth_pb.AuthorizationHeader.deserializeBinary(new Uint8Array(buffer_arg));
+}
+
+function serialize_auth_SessionValidationResponse(arg) {
+  if (!(arg instanceof auth_pb.SessionValidationResponse)) {
+    throw new Error('Expected argument of type auth.SessionValidationResponse');
+  }
+  return Buffer.from(arg.serializeBinary());
+}
+
+function deserialize_auth_SessionValidationResponse(buffer_arg) {
+  return auth_pb.SessionValidationResponse.deserializeBinary(new Uint8Array(buffer_arg));
+}
+
+
+var SessionsService = exports.SessionsService = {
+  validate: {
+    path: '/auth.Sessions/validate',
+    requestStream: false,
+    responseStream: false,
+    requestType: auth_pb.AuthorizationHeader,
+    responseType: auth_pb.SessionValidationResponse,
+    requestSerialize: serialize_auth_AuthorizationHeader,
+    requestDeserialize: deserialize_auth_AuthorizationHeader,
+    responseSerialize: serialize_auth_SessionValidationResponse,
+    responseDeserialize: deserialize_auth_SessionValidationResponse,
+  },
+};
+
+exports.SessionsClient = grpc.makeGenericClientConstructor(SessionsService);

+ 47 - 0
packages/grpc/lib/auth_pb.d.ts

@@ -0,0 +1,47 @@
+// package: auth
+// file: auth.proto
+
+/* tslint:disable */
+/* eslint-disable */
+
+import * as jspb from "google-protobuf";
+
+export class AuthorizationHeader extends jspb.Message { 
+    getBearerToken(): string;
+    setBearerToken(value: string): AuthorizationHeader;
+
+    serializeBinary(): Uint8Array;
+    toObject(includeInstance?: boolean): AuthorizationHeader.AsObject;
+    static toObject(includeInstance: boolean, msg: AuthorizationHeader): AuthorizationHeader.AsObject;
+    static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
+    static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
+    static serializeBinaryToWriter(message: AuthorizationHeader, writer: jspb.BinaryWriter): void;
+    static deserializeBinary(bytes: Uint8Array): AuthorizationHeader;
+    static deserializeBinaryFromReader(message: AuthorizationHeader, reader: jspb.BinaryReader): AuthorizationHeader;
+}
+
+export namespace AuthorizationHeader {
+    export type AsObject = {
+        bearerToken: string,
+    }
+}
+
+export class SessionValidationResponse extends jspb.Message { 
+    getCrossServiceToken(): string;
+    setCrossServiceToken(value: string): SessionValidationResponse;
+
+    serializeBinary(): Uint8Array;
+    toObject(includeInstance?: boolean): SessionValidationResponse.AsObject;
+    static toObject(includeInstance: boolean, msg: SessionValidationResponse): SessionValidationResponse.AsObject;
+    static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
+    static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
+    static serializeBinaryToWriter(message: SessionValidationResponse, writer: jspb.BinaryWriter): void;
+    static deserializeBinary(bytes: Uint8Array): SessionValidationResponse;
+    static deserializeBinaryFromReader(message: SessionValidationResponse, reader: jspb.BinaryReader): SessionValidationResponse;
+}
+
+export namespace SessionValidationResponse {
+    export type AsObject = {
+        crossServiceToken: string,
+    }
+}

+ 328 - 0
packages/grpc/lib/auth_pb.js

@@ -0,0 +1,328 @@
+// source: auth.proto
+/**
+ * @fileoverview
+ * @enhanceable
+ * @suppress {missingRequire} reports error on implicit type usages.
+ * @suppress {messageConventions} JS Compiler reports an error if a variable or
+ *     field starts with 'MSG_' and isn't a translatable message.
+ * @public
+ */
+// GENERATED CODE -- DO NOT EDIT!
+/* eslint-disable */
+// @ts-nocheck
+
+var jspb = require('google-protobuf');
+var goog = jspb;
+var global = (function() {
+  if (this) { return this; }
+  if (typeof window !== 'undefined') { return window; }
+  if (typeof global !== 'undefined') { return global; }
+  if (typeof self !== 'undefined') { return self; }
+  return Function('return this')();
+}.call(null));
+
+goog.exportSymbol('proto.auth.AuthorizationHeader', null, global);
+goog.exportSymbol('proto.auth.SessionValidationResponse', null, global);
+/**
+ * Generated by JsPbCodeGenerator.
+ * @param {Array=} opt_data Optional initial data array, typically from a
+ * server response, or constructed directly in Javascript. The array is used
+ * in place and becomes part of the constructed object. It is not cloned.
+ * If no data is provided, the constructed object will be empty, but still
+ * valid.
+ * @extends {jspb.Message}
+ * @constructor
+ */
+proto.auth.AuthorizationHeader = function(opt_data) {
+  jspb.Message.initialize(this, opt_data, 0, -1, null, null);
+};
+goog.inherits(proto.auth.AuthorizationHeader, jspb.Message);
+if (goog.DEBUG && !COMPILED) {
+  /**
+   * @public
+   * @override
+   */
+  proto.auth.AuthorizationHeader.displayName = 'proto.auth.AuthorizationHeader';
+}
+/**
+ * Generated by JsPbCodeGenerator.
+ * @param {Array=} opt_data Optional initial data array, typically from a
+ * server response, or constructed directly in Javascript. The array is used
+ * in place and becomes part of the constructed object. It is not cloned.
+ * If no data is provided, the constructed object will be empty, but still
+ * valid.
+ * @extends {jspb.Message}
+ * @constructor
+ */
+proto.auth.SessionValidationResponse = function(opt_data) {
+  jspb.Message.initialize(this, opt_data, 0, -1, null, null);
+};
+goog.inherits(proto.auth.SessionValidationResponse, jspb.Message);
+if (goog.DEBUG && !COMPILED) {
+  /**
+   * @public
+   * @override
+   */
+  proto.auth.SessionValidationResponse.displayName = 'proto.auth.SessionValidationResponse';
+}
+
+
+
+if (jspb.Message.GENERATE_TO_OBJECT) {
+/**
+ * Creates an object representation of this proto.
+ * Field names that are reserved in JavaScript and will be renamed to pb_name.
+ * Optional fields that are not set will be set to undefined.
+ * To access a reserved field use, foo.pb_<name>, eg, foo.pb_default.
+ * For the list of reserved names please see:
+ *     net/proto2/compiler/js/internal/generator.cc#kKeyword.
+ * @param {boolean=} opt_includeInstance Deprecated. whether to include the
+ *     JSPB instance for transitional soy proto support:
+ *     http://goto/soy-param-migration
+ * @return {!Object}
+ */
+proto.auth.AuthorizationHeader.prototype.toObject = function(opt_includeInstance) {
+  return proto.auth.AuthorizationHeader.toObject(opt_includeInstance, this);
+};
+
+
+/**
+ * Static version of the {@see toObject} method.
+ * @param {boolean|undefined} includeInstance Deprecated. Whether to include
+ *     the JSPB instance for transitional soy proto support:
+ *     http://goto/soy-param-migration
+ * @param {!proto.auth.AuthorizationHeader} msg The msg instance to transform.
+ * @return {!Object}
+ * @suppress {unusedLocalVariables} f is only used for nested messages
+ */
+proto.auth.AuthorizationHeader.toObject = function(includeInstance, msg) {
+  var f, obj = {
+    bearerToken: jspb.Message.getFieldWithDefault(msg, 1, "")
+  };
+
+  if (includeInstance) {
+    obj.$jspbMessageInstance = msg;
+  }
+  return obj;
+};
+}
+
+
+/**
+ * Deserializes binary data (in protobuf wire format).
+ * @param {jspb.ByteSource} bytes The bytes to deserialize.
+ * @return {!proto.auth.AuthorizationHeader}
+ */
+proto.auth.AuthorizationHeader.deserializeBinary = function(bytes) {
+  var reader = new jspb.BinaryReader(bytes);
+  var msg = new proto.auth.AuthorizationHeader;
+  return proto.auth.AuthorizationHeader.deserializeBinaryFromReader(msg, reader);
+};
+
+
+/**
+ * Deserializes binary data (in protobuf wire format) from the
+ * given reader into the given message object.
+ * @param {!proto.auth.AuthorizationHeader} msg The message object to deserialize into.
+ * @param {!jspb.BinaryReader} reader The BinaryReader to use.
+ * @return {!proto.auth.AuthorizationHeader}
+ */
+proto.auth.AuthorizationHeader.deserializeBinaryFromReader = function(msg, reader) {
+  while (reader.nextField()) {
+    if (reader.isEndGroup()) {
+      break;
+    }
+    var field = reader.getFieldNumber();
+    switch (field) {
+    case 1:
+      var value = /** @type {string} */ (reader.readString());
+      msg.setBearerToken(value);
+      break;
+    default:
+      reader.skipField();
+      break;
+    }
+  }
+  return msg;
+};
+
+
+/**
+ * Serializes the message to binary data (in protobuf wire format).
+ * @return {!Uint8Array}
+ */
+proto.auth.AuthorizationHeader.prototype.serializeBinary = function() {
+  var writer = new jspb.BinaryWriter();
+  proto.auth.AuthorizationHeader.serializeBinaryToWriter(this, writer);
+  return writer.getResultBuffer();
+};
+
+
+/**
+ * Serializes the given message to binary data (in protobuf wire
+ * format), writing to the given BinaryWriter.
+ * @param {!proto.auth.AuthorizationHeader} message
+ * @param {!jspb.BinaryWriter} writer
+ * @suppress {unusedLocalVariables} f is only used for nested messages
+ */
+proto.auth.AuthorizationHeader.serializeBinaryToWriter = function(message, writer) {
+  var f = undefined;
+  f = message.getBearerToken();
+  if (f.length > 0) {
+    writer.writeString(
+      1,
+      f
+    );
+  }
+};
+
+
+/**
+ * optional string bearer_token = 1;
+ * @return {string}
+ */
+proto.auth.AuthorizationHeader.prototype.getBearerToken = function() {
+  return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, ""));
+};
+
+
+/**
+ * @param {string} value
+ * @return {!proto.auth.AuthorizationHeader} returns this
+ */
+proto.auth.AuthorizationHeader.prototype.setBearerToken = function(value) {
+  return jspb.Message.setProto3StringField(this, 1, value);
+};
+
+
+
+
+
+if (jspb.Message.GENERATE_TO_OBJECT) {
+/**
+ * Creates an object representation of this proto.
+ * Field names that are reserved in JavaScript and will be renamed to pb_name.
+ * Optional fields that are not set will be set to undefined.
+ * To access a reserved field use, foo.pb_<name>, eg, foo.pb_default.
+ * For the list of reserved names please see:
+ *     net/proto2/compiler/js/internal/generator.cc#kKeyword.
+ * @param {boolean=} opt_includeInstance Deprecated. whether to include the
+ *     JSPB instance for transitional soy proto support:
+ *     http://goto/soy-param-migration
+ * @return {!Object}
+ */
+proto.auth.SessionValidationResponse.prototype.toObject = function(opt_includeInstance) {
+  return proto.auth.SessionValidationResponse.toObject(opt_includeInstance, this);
+};
+
+
+/**
+ * Static version of the {@see toObject} method.
+ * @param {boolean|undefined} includeInstance Deprecated. Whether to include
+ *     the JSPB instance for transitional soy proto support:
+ *     http://goto/soy-param-migration
+ * @param {!proto.auth.SessionValidationResponse} msg The msg instance to transform.
+ * @return {!Object}
+ * @suppress {unusedLocalVariables} f is only used for nested messages
+ */
+proto.auth.SessionValidationResponse.toObject = function(includeInstance, msg) {
+  var f, obj = {
+    crossServiceToken: jspb.Message.getFieldWithDefault(msg, 1, "")
+  };
+
+  if (includeInstance) {
+    obj.$jspbMessageInstance = msg;
+  }
+  return obj;
+};
+}
+
+
+/**
+ * Deserializes binary data (in protobuf wire format).
+ * @param {jspb.ByteSource} bytes The bytes to deserialize.
+ * @return {!proto.auth.SessionValidationResponse}
+ */
+proto.auth.SessionValidationResponse.deserializeBinary = function(bytes) {
+  var reader = new jspb.BinaryReader(bytes);
+  var msg = new proto.auth.SessionValidationResponse;
+  return proto.auth.SessionValidationResponse.deserializeBinaryFromReader(msg, reader);
+};
+
+
+/**
+ * Deserializes binary data (in protobuf wire format) from the
+ * given reader into the given message object.
+ * @param {!proto.auth.SessionValidationResponse} msg The message object to deserialize into.
+ * @param {!jspb.BinaryReader} reader The BinaryReader to use.
+ * @return {!proto.auth.SessionValidationResponse}
+ */
+proto.auth.SessionValidationResponse.deserializeBinaryFromReader = function(msg, reader) {
+  while (reader.nextField()) {
+    if (reader.isEndGroup()) {
+      break;
+    }
+    var field = reader.getFieldNumber();
+    switch (field) {
+    case 1:
+      var value = /** @type {string} */ (reader.readString());
+      msg.setCrossServiceToken(value);
+      break;
+    default:
+      reader.skipField();
+      break;
+    }
+  }
+  return msg;
+};
+
+
+/**
+ * Serializes the message to binary data (in protobuf wire format).
+ * @return {!Uint8Array}
+ */
+proto.auth.SessionValidationResponse.prototype.serializeBinary = function() {
+  var writer = new jspb.BinaryWriter();
+  proto.auth.SessionValidationResponse.serializeBinaryToWriter(this, writer);
+  return writer.getResultBuffer();
+};
+
+
+/**
+ * Serializes the given message to binary data (in protobuf wire
+ * format), writing to the given BinaryWriter.
+ * @param {!proto.auth.SessionValidationResponse} message
+ * @param {!jspb.BinaryWriter} writer
+ * @suppress {unusedLocalVariables} f is only used for nested messages
+ */
+proto.auth.SessionValidationResponse.serializeBinaryToWriter = function(message, writer) {
+  var f = undefined;
+  f = message.getCrossServiceToken();
+  if (f.length > 0) {
+    writer.writeString(
+      1,
+      f
+    );
+  }
+};
+
+
+/**
+ * optional string cross_service_token = 1;
+ * @return {string}
+ */
+proto.auth.SessionValidationResponse.prototype.getCrossServiceToken = function() {
+  return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, ""));
+};
+
+
+/**
+ * @param {string} value
+ * @return {!proto.auth.SessionValidationResponse} returns this
+ */
+proto.auth.SessionValidationResponse.prototype.setCrossServiceToken = function(value) {
+  return jspb.Message.setProto3StringField(this, 1, value);
+};
+
+
+goog.object.extend(exports, proto.auth);

+ 2 - 0
packages/grpc/lib/index.d.ts

@@ -0,0 +1,2 @@
+export * from './auth_grpc_pb'
+export * from './auth_pb'

+ 18 - 0
packages/grpc/lib/index.js

@@ -0,0 +1,18 @@
+"use strict";
+var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
+    if (k2 === undefined) k2 = k;
+    var desc = Object.getOwnPropertyDescriptor(m, k);
+    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
+      desc = { enumerable: true, get: function() { return m[k]; } };
+    }
+    Object.defineProperty(o, k2, desc);
+}) : (function(o, m, k, k2) {
+    if (k2 === undefined) k2 = k;
+    o[k2] = m[k];
+}));
+var __exportStar = (this && this.__exportStar) || function(m, exports) {
+    for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+__exportStar(require("./auth_grpc_pb"), exports);
+__exportStar(require("./auth_pb"), exports);

+ 33 - 0
packages/grpc/package.json

@@ -0,0 +1,33 @@
+{
+  "name": "@standardnotes/grpc",
+  "version": "1.0.0",
+  "engines": {
+    "node": ">=18.0.0 <21.0.0"
+  },
+  "description": "gRPC definitions for Standard Notes",
+  "author": "Standard Notes",
+  "main": "lib/index.js",
+  "types": "lib/index.d.ts",
+  "files": [
+    "lib/**/*.js",
+    "lib/**/*.d.ts"
+  ],
+  "publishConfig": {
+    "access": "public"
+  },
+  "license": "AGPL-3.0-or-later",
+  "scripts": {
+    "clean": "rm -fr dist",
+    "build": "tsc --build"
+  },
+  "dependencies": {
+    "@grpc/grpc-js": "^1.9.10",
+    "google-protobuf": "^3.21.2"
+  },
+  "devDependencies": {
+    "@types/google-protobuf": "^3",
+    "grpc-tools": "^1.12.4",
+    "grpc_tools_node_protoc_ts": "^5.3.3",
+    "typescript": "^5.0.4"
+  }
+}

+ 15 - 0
packages/grpc/proto/auth.proto

@@ -0,0 +1,15 @@
+syntax = "proto3";
+
+package auth;
+
+message AuthorizationHeader {
+  string bearer_token = 1;
+}
+
+message SessionValidationResponse {
+  string cross_service_token = 1;
+}
+
+service Sessions {
+  rpc validate(AuthorizationHeader) returns (SessionValidationResponse) {}
+}

+ 20 - 0
packages/grpc/scripts/build-protos.sh

@@ -0,0 +1,20 @@
+#!/bin/bash
+
+PROTO_DEST=./lib
+
+mkdir -p ${PROTO_DEST}
+
+# generate js codes via grpc-tools
+yarn run grpc_tools_node_protoc \
+    --js_out=import_style=commonjs,binary:${PROTO_DEST} \
+    --grpc_out=${PROTO_DEST} \
+    --plugin=protoc-gen-grpc=../../.yarn/unplugged/grpc-tools-npm-1.12.4-956df6794d/node_modules/grpc-tools/bin/protoc_plugin.js \
+    -I ./proto \
+    proto/*.proto
+
+# generate d.ts codes
+yarn run grpc_tools_node_protoc \
+    --plugin=protoc-gen-ts=../../.yarn/unplugged/grpc_tools_node_protoc_ts-npm-5.3.3-297a345c26/node_modules/grpc_tools_node_protoc_ts/bin/protoc-gen-ts \
+    --ts_out=${PROTO_DEST} \
+    -I ./proto \
+    proto/*.proto

+ 9 - 0
packages/grpc/tsconfig.json

@@ -0,0 +1,9 @@
+{
+  "extends": "../../tsconfig.json",
+  "compilerOptions": {
+    "composite": true,
+    "outDir": "./lib",
+  },
+  "include": [],
+  "references": []
+}

+ 3 - 1
packages/home-server/src/Server/HomeServer.ts

@@ -176,7 +176,9 @@ export class HomeServer implements HomeServerInterface {
 
       const serverInstance = server.build().listen(port)
 
-      const keepAliveTimeout = env.get('KEEP_ALIVE_TIMEOUT', true) ? +env.get('KEEP_ALIVE_TIMEOUT', true) : 5000
+      const keepAliveTimeout = env.get('HTTP_KEEP_ALIVE_TIMEOUT', true)
+        ? +env.get('HTTP_KEEP_ALIVE_TIMEOUT', true)
+        : 5000
 
       serverInstance.keepAliveTimeout = keepAliveTimeout
 

+ 1 - 1
packages/revisions/bin/server.ts

@@ -45,7 +45,7 @@ void container.load().then((container) => {
 
   const serverInstance = server.build().listen(env.get('PORT'))
 
-  const keepAliveTimeout = env.get('KEEP_ALIVE_TIMEOUT', true) ? +env.get('KEEP_ALIVE_TIMEOUT', true) : 5000
+  const keepAliveTimeout = env.get('HTTP_KEEP_ALIVE_TIMEOUT', true) ? +env.get('HTTP_KEEP_ALIVE_TIMEOUT', true) : 5000
 
   serverInstance.keepAliveTimeout = keepAliveTimeout
 

+ 1 - 1
packages/syncing-server/bin/server.ts

@@ -74,7 +74,7 @@ void container.load().then((container) => {
 
   const serverInstance = server.build().listen(env.get('PORT'))
 
-  const keepAliveTimeout = env.get('KEEP_ALIVE_TIMEOUT', true) ? +env.get('KEEP_ALIVE_TIMEOUT', true) : 5000
+  const keepAliveTimeout = env.get('HTTP_KEEP_ALIVE_TIMEOUT', true) ? +env.get('HTTP_KEEP_ALIVE_TIMEOUT', true) : 5000
 
   serverInstance.keepAliveTimeout = keepAliveTimeout
 

+ 1 - 1
packages/websockets/bin/server.ts

@@ -46,7 +46,7 @@ void container.load().then((container) => {
 
   const serverInstance = server.build().listen(env.get('PORT'))
 
-  const keepAliveTimeout = env.get('KEEP_ALIVE_TIMEOUT', true) ? +env.get('KEEP_ALIVE_TIMEOUT', true) : 5000
+  const keepAliveTimeout = env.get('HTTP_KEEP_ALIVE_TIMEOUT', true) ? +env.get('HTTP_KEEP_ALIVE_TIMEOUT', true) : 5000
 
   serverInstance.keepAliveTimeout = keepAliveTimeout
 

+ 3 - 0
tsconfig.json

@@ -47,6 +47,9 @@
     {
       "path": "./packages/files"
     },
+    {
+      "path": "./packages/grpc"
+    },
     {
       "path": "./packages/predicates"
     },

+ 97 - 1
yarn.lock

@@ -2341,6 +2341,16 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@grpc/grpc-js@npm:^1.9.10":
+  version: 1.9.10
+  resolution: "@grpc/grpc-js@npm:1.9.10"
+  dependencies:
+    "@grpc/proto-loader": "npm:^0.7.8"
+    "@types/node": "npm:>=12.12.47"
+  checksum: 243cf994e6487ec9b6b24b155468c598e7f100491b3a274a963abd897b751d831149362b3c47c2d7c905df3d0fd0fa294ce8cba33efdde61f48e326b6e657e06
+  languageName: node
+  linkType: hard
+
 "@grpc/proto-loader@npm:^0.7.8":
   version: 0.7.10
   resolution: "@grpc/proto-loader@npm:0.7.10"
@@ -2980,6 +2990,25 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@mapbox/node-pre-gyp@npm:^1.0.5":
+  version: 1.0.11
+  resolution: "@mapbox/node-pre-gyp@npm:1.0.11"
+  dependencies:
+    detect-libc: "npm:^2.0.0"
+    https-proxy-agent: "npm:^5.0.0"
+    make-dir: "npm:^3.1.0"
+    node-fetch: "npm:^2.6.7"
+    nopt: "npm:^5.0.0"
+    npmlog: "npm:^5.0.1"
+    rimraf: "npm:^3.0.2"
+    semver: "npm:^7.3.5"
+    tar: "npm:^6.1.11"
+  bin:
+    node-pre-gyp: bin/node-pre-gyp
+  checksum: 59529a2444e44fddb63057152452b00705aa58059079191126c79ac1388ae4565625afa84ed4dd1bf017d1111ab6e47907f7c5192e06d83c9496f2f3e708680a
+  languageName: node
+  linkType: hard
+
 "@nodelib/fs.scandir@npm:2.1.5":
   version: 2.1.5
   resolution: "@nodelib/fs.scandir@npm:2.1.5"
@@ -5235,9 +5264,11 @@ __metadata:
   version: 0.0.0-use.local
   resolution: "@standardnotes/api-gateway@workspace:packages/api-gateway"
   dependencies:
+    "@grpc/grpc-js": "npm:^1.9.10"
     "@standardnotes/domain-core": "workspace:^"
     "@standardnotes/domain-events": "workspace:*"
     "@standardnotes/domain-events-infra": "workspace:*"
+    "@standardnotes/grpc": "workspace:^"
     "@standardnotes/security": "workspace:*"
     "@standardnotes/time": "workspace:*"
     "@types/cors": "npm:^2.8.9"
@@ -5296,6 +5327,7 @@ __metadata:
     "@aws-sdk/client-sqs": "npm:^3.427.0"
     "@cbor-extract/cbor-extract-linux-arm64": "npm:^2.1.1"
     "@cbor-extract/cbor-extract-linux-x64": "npm:^2.1.1"
+    "@grpc/grpc-js": "npm:^1.9.10"
     "@simplewebauthn/server": "npm:^8.1.1"
     "@simplewebauthn/typescript-types": "npm:^8.0.0"
     "@standardnotes/api": "npm:^1.26.26"
@@ -5304,6 +5336,7 @@ __metadata:
     "@standardnotes/domain-events": "workspace:*"
     "@standardnotes/domain-events-infra": "workspace:*"
     "@standardnotes/features": "npm:^1.59.7"
+    "@standardnotes/grpc": "workspace:^"
     "@standardnotes/predicates": "workspace:*"
     "@standardnotes/responses": "npm:^1.13.27"
     "@standardnotes/security": "workspace:*"
@@ -5504,6 +5537,19 @@ __metadata:
   languageName: unknown
   linkType: soft
 
+"@standardnotes/grpc@workspace:^, @standardnotes/grpc@workspace:packages/grpc":
+  version: 0.0.0-use.local
+  resolution: "@standardnotes/grpc@workspace:packages/grpc"
+  dependencies:
+    "@grpc/grpc-js": "npm:^1.9.10"
+    "@types/google-protobuf": "npm:^3"
+    google-protobuf: "npm:^3.21.2"
+    grpc-tools: "npm:^1.12.4"
+    grpc_tools_node_protoc_ts: "npm:^5.3.3"
+    typescript: "npm:^5.0.4"
+  languageName: unknown
+  linkType: soft
+
 "@standardnotes/home-server@workspace:packages/home-server":
   version: 0.0.0-use.local
   resolution: "@standardnotes/home-server@workspace:packages/home-server"
@@ -5691,6 +5737,11 @@ __metadata:
     ini: "npm:^4.1.1"
     ts-node: "npm:^10.9.1"
     typescript: "npm:^5.0.4"
+  dependenciesMeta:
+    grpc-tools@1.12.4:
+      unplugged: true
+    grpc_tools_node_protoc_ts@5.3.3:
+      unplugged: true
   languageName: unknown
   linkType: soft
 
@@ -6093,6 +6144,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@types/google-protobuf@npm:^3":
+  version: 3.15.10
+  resolution: "@types/google-protobuf@npm:3.15.10"
+  checksum: 29efde966ff47ae824ffe2bccfc999a3adf355745a606a24177ada0e6b7892ea49b2fdd0355400c2140dbd9175bf1d1109f20ce6fdb40ec833724ade439d03d6
+  languageName: node
+  linkType: hard
+
 "@types/graceful-fs@npm:^4.1.3":
   version: 4.1.6
   resolution: "@types/graceful-fs@npm:4.1.6"
@@ -9460,6 +9518,20 @@ __metadata:
   languageName: node
   linkType: hard
 
+"google-protobuf@npm:3.15.8":
+  version: 3.15.8
+  resolution: "google-protobuf@npm:3.15.8"
+  checksum: 0b1ea24a552e0870b658b975ce5090c13f35793dc4ff8a733ad9c27990126eb9bd375fa9d3204af4f634930ae358d8b5e78f9f4cdb1f700e35b4d857f0aac742
+  languageName: node
+  linkType: hard
+
+"google-protobuf@npm:^3.21.2":
+  version: 3.21.2
+  resolution: "google-protobuf@npm:3.21.2"
+  checksum: b376c2e47fb0419b41b901e4da8f3827fe9594ffb7887708b9c241f36005d0b9f2edc7b3f05795f6793924a241e767f67831732eae0f23bdbb337b56a6ab4e26
+  languageName: node
+  linkType: hard
+
 "graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.11, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9":
   version: 4.2.11
   resolution: "graceful-fs@npm:4.2.11"
@@ -9474,7 +9546,31 @@ __metadata:
   languageName: node
   linkType: hard
 
-"handlebars@npm:^4.7.7":
+"grpc-tools@npm:^1.12.4":
+  version: 1.12.4
+  resolution: "grpc-tools@npm:1.12.4"
+  dependencies:
+    "@mapbox/node-pre-gyp": "npm:^1.0.5"
+  bin:
+    grpc_tools_node_protoc: bin/protoc.js
+    grpc_tools_node_protoc_plugin: bin/protoc_plugin.js
+  checksum: 56852c756fe34be5f3b2cf75b1d432886b17ad32d438cacbcf8d30e980c7361a8e8325b1e626ce99826b8dbf1364145bedbedeb6d7feb916c4801481810a05d1
+  languageName: node
+  linkType: hard
+
+"grpc_tools_node_protoc_ts@npm:^5.3.3":
+  version: 5.3.3
+  resolution: "grpc_tools_node_protoc_ts@npm:5.3.3"
+  dependencies:
+    google-protobuf: "npm:3.15.8"
+    handlebars: "npm:4.7.7"
+  bin:
+    protoc-gen-ts: bin/protoc-gen-ts
+  checksum: 96fe57b04a7d9e7e656678fab14dd4fc8027849abbb487a7eae4d2841651aa9a481402d5b9df34b4ac3bac6f190551db2e647127d6f6736d5f89ea2fa791ff33
+  languageName: node
+  linkType: hard
+
+"handlebars@npm:4.7.7, handlebars@npm:^4.7.7":
   version: 4.7.7
   resolution: "handlebars@npm:4.7.7"
   dependencies: