From 9bd1f1924c5924a7170577786bcb7d1c68759379 Mon Sep 17 00:00:00 2001 From: Bruno Mateus Date: Mon, 19 Aug 2024 12:43:30 +0100 Subject: [PATCH] feat(satp-hermes): gateway runner and docker for SATP Signed-off-by: Bruno Mateus --- .../{src/main/typescript => }/.env.example | 21 +- .../Dockerfile.gateway | 71 +++ .../docker-compose.yml | 54 +++ packages/cactus-plugin-satp-hermes/makefile | 48 +++ .../cactus-plugin-satp-hermes/package.json | 11 +- .../run-satp-gateway.sh | 36 ++ .../plugin-satp-hermes-gateway-cli.ts | 41 +- .../typescript/plugin-satp-hermes-gateway.ts | 408 +++++++++++++++--- .../integration/gateway-init-startup.test.ts | 6 +- .../unit/constructor-instantiation.test.ts | 58 +++ .../supervisord.conf | 19 + .../src/main/typescript/public-api.ts | 5 + .../typescript/satp/satp-gateway-runner.ts | 302 +++++++++++++ yarn.lock | 1 + 14 files changed, 983 insertions(+), 98 deletions(-) rename packages/cactus-plugin-satp-hermes/{src/main/typescript => }/.env.example (57%) create mode 100644 packages/cactus-plugin-satp-hermes/Dockerfile.gateway create mode 100644 packages/cactus-plugin-satp-hermes/docker-compose.yml create mode 100644 packages/cactus-plugin-satp-hermes/makefile create mode 100644 packages/cactus-plugin-satp-hermes/run-satp-gateway.sh create mode 100644 packages/cactus-plugin-satp-hermes/src/test/typescript/unit/constructor-instantiation.test.ts create mode 100644 packages/cactus-plugin-satp-hermes/supervisord.conf create mode 100644 packages/cactus-test-tooling/src/main/typescript/satp/satp-gateway-runner.ts diff --git a/packages/cactus-plugin-satp-hermes/src/main/typescript/.env.example b/packages/cactus-plugin-satp-hermes/.env.example similarity index 57% rename from packages/cactus-plugin-satp-hermes/src/main/typescript/.env.example rename to packages/cactus-plugin-satp-hermes/.env.example index 5f11c435e1..eb22fc37fb 100644 --- a/packages/cactus-plugin-satp-hermes/src/main/typescript/.env.example +++ b/packages/cactus-plugin-satp-hermes/.env.example @@ -2,37 +2,38 @@ SATP_PRIVATE_KEY=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef SATP_PUBLIC_KEY=fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210 - # SATP Gateway Configuration SATP_LOG_LEVEL=INFO - -# Environment and API SATP_NODE_ENV=development SATP_ENABLE_OPEN_API=true # Gateway Identity SATP_GATEWAY_ID=gateway1 SATP_GATEWAY_NAME=ExampleGateway -SATP_GATEWAY_VERSION=v02,v02,v02 SATP_SUPPORTED_DLTS=FabricSATPGateway,BesuSATPGateway +# Detailed Version Configuration +SATP_GATEWAY_VERSION_CORE=v02 +SATP_GATEWAY_VERSION_ARCHITECTURE=v02 +SATP_GATEWAY_VERSION_CRASH=v02 + # Proof and Ports -SATP_PROOF_ID=proof123 +SATP_PROOF_ID=mockProofID1 SATP_GATEWAY_SERVER_PORT=3010 SATP_GATEWAY_CLIENT_PORT=3011 -SATP_GATEWAY_GRPC_PORT=3012 +SATP_GATEWAY_GRPC_PORT=4010 # Gateway Address -SATP_GATEWAY_ADDRESS=http://localhost:3010 +SATP_GATEWAY_ADDRESS=http://localhost # Counter Party Gateways (JSON array) -SATP_COUNTER_PARTY_GATEWAYS=[{"id":"gateway2","name":"OtherGateway","address":"http://other-gateway:3010"}] +SATP_COUNTER_PARTY_GATEWAYS=[{"id":"gateway2","version":"v02,v02,v02","supportedDLTs":"FabricSATPGateway,BesuSATPGateway"}] #"http://other-gateway:3010"}] # Validation Options (JSON object) SATP_VALIDATION_OPTIONS={"skipMissingProperties":true} # Privacy Policies (JSON array) -SATP_PRIVACY_POLICIES=[{"policy":"GDPR","policyHash":"hash123"}] +SATP_PRIVACY_POLICIES=[{"policy":"singleTransaction","policyHash":"hash123"}] # Merge Policies (JSON array) -SATP_MERGE_POLICIES=[{"policy":"LatestWins","policyHash":"hash456"}] +SATP_MERGE_POLICIES=[{"policy":"NONE","policyHash":"hash456"}] diff --git a/packages/cactus-plugin-satp-hermes/Dockerfile.gateway b/packages/cactus-plugin-satp-hermes/Dockerfile.gateway new file mode 100644 index 0000000000..cd3bdb1866 --- /dev/null +++ b/packages/cactus-plugin-satp-hermes/Dockerfile.gateway @@ -0,0 +1,71 @@ +FROM ubuntu:22.04 + +USER root +RUN groupadd -r docker +RUN usermod -aG docker root + +RUN apt-get update +RUN apt-get -y install --no-install-recommends supervisor +RUN apt-get -y install --no-install-recommends openssl +RUN apt-get -y install --no-install-recommends jq +RUN apt-get -y install --no-install-recommends curl +RUN apt-get -y install --no-install-recommends file +RUN apt-get -y install --no-install-recommends ca-certificates +RUN apt-get -y install --no-install-recommends tzdata +RUN apt-get -y install --no-install-recommends git +RUN apt-get -y install --no-install-recommends apt-utils +RUN apt-get -y install --no-install-recommends net-tools +RUN apt-get -y install --no-install-recommends default-jdk + +RUN mkdir -p /var/log/supervisor +COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf + +ARG APP=/usr/src/app/cactus/ +COPY . ${APP} + +RUN mkdir -p ${APP} +RUN mkdir -p "${APP}/log/" +RUN mkdir -p /app/keys + +WORKDIR ${APP} + +SHELL ["/bin/bash", "--login", "-i", "-c"] +# Installing Node Version Manager (nvm) +RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash + +RUN source ~/.bashrc && \ + nvm install 18.18.2 && \ + npm install -g yarn ts-node + +SHELL ["/bin/bash", "--login", "-c"] + +COPY ./supervisord.conf /etc/supervisord.conf +COPY ./run-satp-gateway.sh / +COPY ./tsconfig.json /usr/src/tsconfig.json +COPY ./tsconfig.json /tsconfig.json + +RUN chmod +x /run-satp-gateway.sh + +EXPOSE 3010 3011 4010 + +EXPOSE 9001 + +ENV SATP_PRIVATE_KEY="default_private_key" +ENV SATP_PUBLIC_KEY="default_public_key" + +ENV SATP_LOG_LEVEL=INFO +ENV SATP_NODE_ENV=development +ENV SATP_ENABLE_OPEN_API=true + +ENV SATP_GATEWAY_VERSION_CORE=v02 +ENV SATP_GATEWAY_VERSION_ARCHITECTURE=v02 +ENV SATP_GATEWAY_VERSION_CRASH=v02 +ENV SATP_GATEWAY_SERVER_PORT=3010 +ENV SATP_GATEWAY_CLIENT_PORT=3011 +ENV SATP_GATEWAY_GRPC_PORT=4010 +ENV SATP_VALIDATION_OPTIONS={"skipMissingProperties":true} +ENV SATP_PRIVACY_POLICIES=[{"policy":"singleTransaction","policyHash":"hash123"}] +ENV SATP_MERGE_POLICIES=[{"policy":"NONE","policyHash":"hash456"}] + +ENTRYPOINT ["/usr/bin/supervisord"] +CMD ["--configuration", "/etc/supervisord.conf", "--nodaemon"] \ No newline at end of file diff --git a/packages/cactus-plugin-satp-hermes/docker-compose.yml b/packages/cactus-plugin-satp-hermes/docker-compose.yml new file mode 100644 index 0000000000..722abc78fe --- /dev/null +++ b/packages/cactus-plugin-satp-hermes/docker-compose.yml @@ -0,0 +1,54 @@ +version: '3.8' + +services: + ### To build new image version: + ### uncomment and run in the terminal "docker compose build" + # satp-hermes-gateway-base: + # image: ghcr.io/brunoffmateus/cactus-plugin-satp-hermes-gateway:new-version + # build: + # context: ./ + # dockerfile: Dockerfile.gateway + + satp-hermes-gateway1: + image: ghcr.io/brunoffmateus/cactus-plugin-satp-hermes-gateway:2024-09-02-3117 + ports: + - 13010:3010/tcp # SERVER_PORT + - 13011:3011/tcp # CLIENT_PORT + environment: + - SATP_GATEWAY_ID=gateway1 + - SATP_GATEWAY_NAME=ExampleGateway1 + - SATP_SUPPORTED_DLTS=FabricSATPGateway,BesuSATPGateway + - SATP_GATEWAY_ADDRESS=http://localhost + - SATP_PROOF_ID=mockProofID1 + - SATP_COUNTER_PARTY_GATEWAYS=[{"id":"gateway2","version":"v02,v02,v02","supportedDLTs":"FabricSATPGateway,BesuSATPGateway"}] + volumes: + - gateway1_keys:/app/keys + deploy: + resources: + limits: + memory: 4G + cpus: '0.9' + + # satp-hermes-gateway2: + # image: ghcr.io/brunoffmateus/cactus-plugin-satp-hermes-gateway:2024-09-02-3117 + # ports: + # - 23020:3010/tcp # SERVER_PORT + # - 23021:3011/tcp # CLIENT_PORT + # environment: + # - SATP_GATEWAY_ID=gateway2 + # - SATP_GATEWAY_NAME=ExampleGateway2 + # - SATP_SUPPORTED_DLTS=FabricSATPGateway,BesuSATPGateway + # - SATP_GATEWAY_ADDRESS=http://localhost + # - SATP_PROOF_ID=mockProofID2 + # - SATP_COUNTER_PARTY_GATEWAYS=[{"id":"gateway1","version":"v02,v02,v02","supportedDLTs":"FabricSATPGateway,BesuSATPGateway"}] + # volumes: + # - gateway2_keys:/app/keys + # deploy: + # resources: + # limits: + # memory: 2G + # cpus: '0.5' + +volumes: + gateway1_keys: + # gateway2_keys: \ No newline at end of file diff --git a/packages/cactus-plugin-satp-hermes/makefile b/packages/cactus-plugin-satp-hermes/makefile new file mode 100644 index 0000000000..0831bba81d --- /dev/null +++ b/packages/cactus-plugin-satp-hermes/makefile @@ -0,0 +1,48 @@ +.PHONY: setup-env run-gateway clean + +setup-env: + @echo "Setting up environment for SATP-Hermes..." + + # Check if Node.js is installed, if not, install it + @if ! command -v node >/dev/null 2>&1; then \ + echo "Installing Node.js 18.18.2..."; \ + sudo curl -fsSL https://nodejs.org/dist/v18.18.2/node-v18.18.2-linux-x64.tar.xz | sudo tar -xJ -C /usr/local --strip-components=1; \ + fi + + @if ! command -v yarn >/dev/null 2>&1; then \ + echo "Installing Yarn..."; \ + sudo npm install -g yarn; \ + fi + + # Install dependencies and compile the plugin + yarn install + yarn tsc + + @echo "Initializing databases..." + yarn db:init + + @echo "Environment setup complete." + +run-gateway: + # @echo "Building SATP Gateway Docker image..." + # docker compose build + + @echo "Running SATP Gateway Docker containers..." + docker compose up + + # @echo "Pushing SATP Gateway Docker image..." + # docker push ${DOCKER_IMAGE} + +clean: + @echo "Cleaning up SATP-Hermes environment..." + + @echo "Rolling back database migrations..." + yarn db:rollback + + @echo "Removing SQLite database files..." + find src/knex -name "*.sqlite3" -type f -delete + + @echo "Stopping and removing Docker containers..." + docker compose down + + @echo "Cleanup complete." \ No newline at end of file diff --git a/packages/cactus-plugin-satp-hermes/package.json b/packages/cactus-plugin-satp-hermes/package.json index c0c1c73e56..441abb2016 100644 --- a/packages/cactus-plugin-satp-hermes/package.json +++ b/packages/cactus-plugin-satp-hermes/package.json @@ -65,6 +65,12 @@ "codegen:openapi": "npm run generate-sdk", "codegen:proto": "npm run generate-proto", "codegen:abi": "yarn forge:all && abi-types-generator './src/solidity/generated/satp-wrapper.sol/SATPWrapperContract.json' --output='./src/main/typescript/generated'", + "db:init": "run-s db:init:local db:init:remote", + "db:init:local": "knex migrate:latest --knexfile src/knex/knexfile.ts", + "db:init:remote": "knex migrate:latest --knexfile src/knex/knexfile-remote.ts", + "db:rollback": "run-s db:rollback:local db:rollback:remote", + "db:rollback:local": "knex migrate:rollback --knexfile src/knex/knexfile.ts", + "db:rollback:remote": "knex migrate:rollback --knexfile src/knex/knexfile-remote.ts", "generate-proto": "cd src/main/proto && buf generate --template buf.gen.yaml --config buf.yaml --verbose", "generate-sdk": "run-p 'generate-sdk:*'", "generate-sdk:typescript-axios-bol": "yarn bundle-openapi-yaml && yarn bundle-openapi-json && openapi-generator-cli generate -i ./src/main/yml/bol/openapi-blo-bundled.yml -g typescript-axios -o ./src/main/typescript/generated/gateway-client/typescript-axios/ --reserved-words-mappings protected=protected --enable-post-process-file", @@ -110,8 +116,8 @@ "crypto-js": "4.2.0", "dotenv": "16.4.5", "ethers": "6.13.1", - "express": "4.17.2", - "fabric-network": "2.2.19", + "express": "4.19.2", + "fabric-network": "2.2.20", "fs-extra": "11.2.0", "google-protobuf": "3.21.2", "hardhat": "2.22.5", @@ -155,6 +161,7 @@ "make-dir-cli": "3.1.0", "protobufjs": "7.2.5", "swagger-cli": "4.0.4", + "ts-node": "10.9.1", "typescript": "5.5.2" }, "engines": { diff --git a/packages/cactus-plugin-satp-hermes/run-satp-gateway.sh b/packages/cactus-plugin-satp-hermes/run-satp-gateway.sh new file mode 100644 index 0000000000..051aee5d48 --- /dev/null +++ b/packages/cactus-plugin-satp-hermes/run-satp-gateway.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +generate_keys() { + # Check if we need to generate new keys + if [ "$SATP_PRIVATE_KEY" = "default_private_key" ] || [ "$SATP_PUBLIC_KEY" = "default_public_key" ]; then + echo "Generating new key pair..." + # Using OpenSSL to generate a 256-bit private key and derive public key + PRIVATE_KEY=$(openssl ecparam -name secp256k1 -genkey -noout | openssl ec -text -noout | grep priv -A 3 | tail -n +2 | tr -d '\n[:space:]:' | sed 's/^00//') + PUBLIC_KEY=$(openssl ecparam -name secp256k1 -genkey -noout | openssl ec -text -noout | grep pub -A 5 | tail -n +2 | tr -d '\n[:space:]:' | sed 's/^04//') + + export SATP_PRIVATE_KEY=$PRIVATE_KEY + export SATP_PUBLIC_KEY=$PUBLIC_KEY + else + echo "Using existing key pair from environment variables" + fi + + echo "SATP_PRIVATE_KEY: $SATP_PRIVATE_KEY" + echo "SATP_PUBLIC_KEY: $SATP_PUBLIC_KEY" +} + +run_gateway() { + export NVM_DIR="$HOME/.nvm" + [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" + nvm use 18.18.2 + + echo "Starting SATP Hermes Gateway CLI..." + node --max-old-space-size=2048 -r ts-node/register /usr/src/app/cactus/packages/cactus-plugin-satp-hermes/src/main/typescript/plugin-satp-hermes-gateway-cli.ts + echo "SATP Hermes Gateway CLI execution completed." +} + +main() { + generate_keys + run_gateway +} + +main \ No newline at end of file diff --git a/packages/cactus-plugin-satp-hermes/src/main/typescript/plugin-satp-hermes-gateway-cli.ts b/packages/cactus-plugin-satp-hermes/src/main/typescript/plugin-satp-hermes-gateway-cli.ts index 7080204cee..25a2548463 100644 --- a/packages/cactus-plugin-satp-hermes/src/main/typescript/plugin-satp-hermes-gateway-cli.ts +++ b/packages/cactus-plugin-satp-hermes/src/main/typescript/plugin-satp-hermes-gateway-cli.ts @@ -2,44 +2,45 @@ import { LoggerProvider, LogLevelDesc } from "@hyperledger/cactus-common"; import { SATPGateway, SATPGatewayConfig } from "./plugin-satp-hermes-gateway"; -import { SupportedChain, DraftVersions, CurrentDrafts } from "./core/types"; +import { SupportedChain } from "./core/types"; +import { SATP_VERSION } from "./core/constants"; +import { v4 as uuidv4 } from "uuid"; import dotenv from "dotenv"; +import path from "path"; export async function launchGateway(env?: NodeJS.ProcessEnv): Promise { - dotenv.config(); + dotenv.config({ path: path.resolve(__dirname, "../../../.env.example") }); const logLevel: LogLevelDesc = - (env?.SATP_LOG_LEVEL as LogLevelDesc) || "INFO"; + (env?.SATP_LOG_LEVEL?.toUpperCase() as LogLevelDesc) || "INFO"; const logger = LoggerProvider.getOrCreate({ level: logLevel, label: "SATP-Gateway", }); - // Parse the version string into DraftVersions object - const parseVersion = (versionString: string): DraftVersions => { - const [Core, Architecture, Crash] = versionString.split(","); - return { - [CurrentDrafts.Core]: Core, - [CurrentDrafts.Architecture]: Architecture, - [CurrentDrafts.Crash]: Crash, - }; + const parseSupportedDLTs = (): SupportedChain[] => { + return env + ?.SATP_SUPPORTED_DLTS!.split(",") + .filter((dlt) => + Object.values(SupportedChain).includes(dlt as SupportedChain), + ) as SupportedChain[]; }; const gatewayConfig: SATPGatewayConfig = { gid: { - id: env?.SATP_GATEWAY_ID || "", + id: env?.SATP_GATEWAY_ID || uuidv4(), name: env?.SATP_GATEWAY_NAME, - version: env?.SATP_GATEWAY_VERSION - ? [parseVersion(env.SATP_GATEWAY_VERSION)] - : [], - supportedDLTs: - env?.SATP_SUPPORTED_DLTS?.split(",").map( - (dlt) => dlt as SupportedChain, - ) || [], + version: [ + { + Core: env?.SATP_GATEWAY_VERSION_CORE || SATP_VERSION, + Architecture: env?.SATP_GATEWAY_VERSION_ARCHITECTURE || SATP_VERSION, + Crash: env?.SATP_GATEWAY_VERSION_CRASH || SATP_VERSION, + }, + ], + supportedDLTs: env?.SATP_SUPPORTED_DLTS ? parseSupportedDLTs() : [], proofID: env?.SATP_PROOF_ID, gatewayServerPort: parseInt(env?.SATP_GATEWAY_SERVER_PORT || "0", 10), gatewayClientPort: parseInt(env?.SATP_GATEWAY_CLIENT_PORT || "0", 10), - gatewayGrpcPort: parseInt(env?.SATP_GATEWAY_GRPC_PORT || "0", 10), address: env?.SATP_GATEWAY_ADDRESS as | `http://${string}` | `https://${string}` diff --git a/packages/cactus-plugin-satp-hermes/src/main/typescript/plugin-satp-hermes-gateway.ts b/packages/cactus-plugin-satp-hermes/src/main/typescript/plugin-satp-hermes-gateway.ts index f3a608d1f7..de692a35fc 100644 --- a/packages/cactus-plugin-satp-hermes/src/main/typescript/plugin-satp-hermes-gateway.ts +++ b/packages/cactus-plugin-satp-hermes/src/main/typescript/plugin-satp-hermes-gateway.ts @@ -6,6 +6,7 @@ import { ILoggerOptions, JsObjectSigner, IJsObjectSignerOptions, + LogLevelDesc, } from "@hyperledger/cactus-common"; import { v4 as uuidv4 } from "uuid"; @@ -15,6 +16,7 @@ import { IsObject, IsString, Contains, + ValidatorOptions, } from "class-validator"; import path from "path"; @@ -24,6 +26,8 @@ import { GatewayIdentity, ShutdownHook, SupportedChain, + Address, + DraftVersions, } from "./core/types"; import { GatewayOrchestrator, @@ -35,6 +39,7 @@ import http from "http"; import { DEFAULT_PORT_GATEWAY_CLIENT, DEFAULT_PORT_GATEWAY_SERVER, + SATP_VERSION, } from "./core/constants"; import { bufArray2HexStr } from "./gateway-utils"; import { @@ -54,6 +59,16 @@ import { SATPBridgesManager, } from "./gol/satp-bridges-manager"; +import dotenv from "dotenv"; +import { IPrivacyPolicyValue } from "@hyperledger/cactus-plugin-bungee-hermes/dist/lib/main/typescript/view-creation/privacy-policies"; +import { + MergePolicyOpts, + PrivacyPolicyOpts, +} from "@hyperledger/cactus-plugin-bungee-hermes/dist/lib/main/typescript/generated/openapi/typescript-axios"; +import { IMergePolicyValue } from "@hyperledger/cactus-plugin-bungee-hermes/dist/lib/main/typescript/view-merging/merge-policies"; +import { ISignerKeyPairs } from "@hyperledger/cactus-common/src/main/typescript/signer-key-pairs"; +dotenv.config({ path: path.resolve(__dirname, "../../../.env.example") }); + export class SATPGateway implements IPluginWebService, ICactusPlugin { // todo more checks; example port from config is between 3000 and 9000 @IsDefined() @@ -249,92 +264,357 @@ export class SATPGateway implements IPluginWebService, ICactusPlugin { } /* Gateway configuration helpers */ - static ProcessGatewayCoordinatorConfig( - pluginOptions: SATPGatewayConfig, - ): SATPGatewayConfig { - if (!pluginOptions.keyPair) { - pluginOptions.keyPair = Secp256k1Keys.generateKeyPairsBuffer(); + + private static processGatewayId(): string { + return process.env.SATP_GATEWAY_ID || uuidv4(); + } + + private static processGatewayName(): string { + return process.env.SATP_GATEWAY_NAME || uuidv4(); + } + + private static processGatewayVersion(): DraftVersions[] { + return [ + { + Core: process.env.SATP_GATEWAY_VERSION_CORE || SATP_VERSION, + Architecture: + process.env.SATP_GATEWAY_VERSION_ARCHITECTURE || SATP_VERSION, + Crash: process.env.SATP_GATEWAY_VERSION_CRASH || SATP_VERSION, + }, + ]; + } + + private static processGatewaySupportedDLTs(): SupportedChain[] { + if (process.env.SATP_SUPPORTED_DLTS) { + const dlts = process.env.SATP_SUPPORTED_DLTS.split(",").filter((dlt) => + Object.values(SupportedChain).includes(dlt as SupportedChain), + ) as SupportedChain[]; + if (dlts.length > 0) { + return dlts; + } + console.warn( + "SATP_SUPPORTED_DLTS is empty or contains no valid DLTs. Using default values.", + ); + } + return []; + } + + private static processGatewayProofID(): string { + return process.env.SATP_PROOF_ID || uuidv4(); + } + + private static processGatewayServerPort(): number { + const port = Number(process.env.SATP_GATEWAY_SERVER_PORT); + if (process.env.SATP_GATEWAY_SERVER_PORT && !isNaN(Number(port))) { + return parseInt(process.env.SATP_GATEWAY_SERVER_PORT); + } + return DEFAULT_PORT_GATEWAY_SERVER; + } + + private static processGatewayClientPort(): number { + const port = Number(process.env.SATP_GATEWAY_CLIENT_PORT); + if (process.env.SATP_GATEWAY_CLIENT_PORT && !isNaN(Number(port))) { + return parseInt(process.env.SATP_GATEWAY_CLIENT_PORT); } + return DEFAULT_PORT_GATEWAY_CLIENT; + } - const id = uuidv4(); + private static processGatewayAddress(): Address { + return ( + (process.env.SATP_GATEWAY_ADDRESS as Address) || + `http://localhost:${DEFAULT_PORT_GATEWAY_CLIENT}` + ); + } + + private static processGatewayIdentity( + pluginOptions: SATPGatewayConfig, + ): GatewayIdentity { if (!pluginOptions.gid) { - pluginOptions.gid = { - id: id, - pubKey: bufArray2HexStr(pluginOptions.keyPair.publicKey), - name: id, - version: [ - { - Core: "v02", - Architecture: "v02", - Crash: "v02", - }, - ], - supportedDLTs: [SupportedChain.FABRIC, SupportedChain.BESU], - proofID: "mockProofID1", - gatewayServerPort: DEFAULT_PORT_GATEWAY_SERVER, - gatewayClientPort: DEFAULT_PORT_GATEWAY_CLIENT, - address: "http://localhost", + return { + id: this.processGatewayId(), + pubKey: bufArray2HexStr(pluginOptions.keyPair!.publicKey), + name: this.processGatewayName(), + version: this.processGatewayVersion(), + supportedDLTs: this.processGatewaySupportedDLTs(), + proofID: this.processGatewayProofID(), + gatewayServerPort: this.processGatewayServerPort(), + gatewayClientPort: this.processGatewayClientPort(), + address: this.processGatewayAddress(), }; } else { - if (!pluginOptions.gid.id) { - pluginOptions.gid.id = id; - } + const gid = pluginOptions.gid; + return { + id: gid.id, // || this.processGatewayId(), + pubKey: bufArray2HexStr(pluginOptions.keyPair!.publicKey), + name: gid.name || this.processGatewayName(), + version: gid.version, // || this.processGatewayVersion(), + supportedDLTs: gid.supportedDLTs, + // gid.supportedDLTs && gid.supportedDLTs.length > 0 + // ? gid.supportedDLTs + // : this.processGatewaySupportedDLTs(), + proofID: gid.proofID || this.processGatewayProofID(), + gatewayServerPort: + gid.gatewayServerPort && gid.gatewayServerPort !== 0 + ? gid.gatewayServerPort + : this.processGatewayServerPort(), + gatewayClientPort: + gid.gatewayClientPort && gid.gatewayClientPort !== 0 + ? gid.gatewayClientPort + : this.processGatewayClientPort(), + address: gid.address || this.processGatewayAddress(), + }; + } + } - if (!pluginOptions.gid.name) { - pluginOptions.gid.name = id; - } + private static processCounterPartyGateways(): GatewayIdentity[] { + if (process.env.SATP_COUNTER_PARTY_GATEWAYS) { + try { + const parsedGateways = JSON.parse( + process.env.SATP_COUNTER_PARTY_GATEWAYS, + ) as GatewayIdentity[]; - if (!pluginOptions.gid.pubKey) { - pluginOptions.gid.pubKey = bufArray2HexStr( - pluginOptions.keyPair.publicKey, + const validGateway = (gateway: unknown): gateway is GatewayIdentity => { + if (!gateway) { + return false; + } + const gw = gateway as Record; + if ( + !("id" in gw) || + !("version" in gw) || + !("supportedDLTs" in gw) || + typeof gw.id !== "string" || + typeof gw.version !== "string" || + typeof gw.supportedDLTs !== "string" + ) { + return false; + } + const [Core, Architecture, Crash] = gw.version.split(","); + if (!Core || !Architecture || !Crash) { + return false; + } + const dlts = gw.supportedDLTs.split(",") as SupportedChain[]; + if ( + !dlts.every((dlt) => Object.values(SupportedChain).includes(dlt)) || + dlts.length === 0 + ) { + return false; + } + // validate optional fields if provided + if ( + ("name" in gw && typeof gw.name !== "string") || + ("proofID" in gw && typeof gw.proofID !== "string") || + ("gatewayServerPort" in gw && + (typeof gw.gatewayServerPort !== "string" || + isNaN(Number(gw.gatewayServerPort)))) || + ("gatewayClientPort" in gw && + (typeof gw.gatewayClientPort !== "string" || + isNaN(Number(gw.gatewayClientPort)))) || + ("address" in gw && typeof gw.address !== "string") + ) { + return false; + } + return true; + }; + + if ( + !Array.isArray(parsedGateways) || + !parsedGateways.every(validGateway) + ) { + throw new Error( + "SATP_COUNTER_PARTY_GATEWAYS must be an array of valid gateway identities", + ); + } else { + return parsedGateways; + } + } catch (error) { + console.warn( + `Failed to parse SATP_COUNTER_PARTY_GATEWAYS: ${error.message}. Using default.`, ); } + } + return []; + } - if (!pluginOptions.gid.version) { - pluginOptions.gid.version = [ - { - Core: "v02", - Architecture: "v02", - Crash: "v02", - }, - ]; - } + private static processLogLevel(): LogLevelDesc { + return ( + (process.env.SATP_LOG_LEVEL?.toUpperCase() as LogLevelDesc) || "DEBUG" + ); + } - if (!pluginOptions.gid.supportedDLTs) { - pluginOptions.gid.supportedDLTs = [ - SupportedChain.FABRIC, - SupportedChain.BESU, - ]; - } + private static processKeyPair(): ISignerKeyPairs { + if (process.env.SATP_PUBLIC_KEY && process.env.SATP_PRIVATE_KEY) { + return { + publicKey: Buffer.from(process.env.SATP_PUBLIC_KEY, "hex"), + privateKey: Buffer.from(process.env.SATP_PRIVATE_KEY, "hex"), + }; + } + return Secp256k1Keys.generateKeyPairsBuffer(); + } - if (!pluginOptions.gid.proofID) { - pluginOptions.gid.proofID = "mockProofID1"; - } + private static processEnvironment(): "development" | "production" { + return ( + (process.env.SATP_NODE_ENV as "development" | "production") || + "development" + ); + } - if (!pluginOptions.gid.gatewayServerPort) { - pluginOptions.gid.gatewayServerPort = DEFAULT_PORT_GATEWAY_SERVER; - } + private static processEnableOpenAPI(): boolean { + if (process.env.SATP_ENABLE_OPEN_API === "false") { + return false; + } + return true; + } - if (!pluginOptions.gid.gatewayClientPort) { - pluginOptions.gid.gatewayClientPort = DEFAULT_PORT_GATEWAY_CLIENT; + private static processValidationOptions(): ValidatorOptions { + if (process.env.SATP_VALIDATION_OPTIONS) { + try { + const envValidationOptions = JSON.parse( + process.env.SATP_VALIDATION_OPTIONS, + ) as ValidatorOptions; + + if ( + typeof envValidationOptions.skipMissingProperties !== "boolean" && + envValidationOptions.skipMissingProperties !== undefined + ) { + throw new Error( + "skipMissingProperties must be a boolean if provided", + ); + } else { + return envValidationOptions; + } + } catch (error) { + console.warn( + `Failed to parse SATP_VALIDATION_OPTIONS: ${error}. Using default.`, + ); } + } + return {}; + } - if (!pluginOptions.logLevel) { - pluginOptions.logLevel = "DEBUG"; + private static processPrivacyPolicies(): IPrivacyPolicyValue[] { + if (process.env.SATP_PRIVACY_POLICIES) { + try { + const parsedPolicies = JSON.parse( + process.env.SATP_PRIVACY_POLICIES, + ) as IPrivacyPolicyValue[]; + + const validPolicies = ( + eachPolicy: unknown, + ): eachPolicy is IPrivacyPolicyValue => { + if (!eachPolicy) { + return false; + } + const policy = eachPolicy as Record; + return ( + "policy" in policy && + "policyHash" in policy && + typeof policy.policy === "string" && + typeof policy.policyHash === "string" && + (policy.policy === PrivacyPolicyOpts.PruneState || + policy.policy === PrivacyPolicyOpts.SingleTransaction) + ); + }; + + if ( + !Array.isArray(parsedPolicies) || + !parsedPolicies.every(validPolicies) + ) { + throw new Error( + "SATP_PRIVACY_POLICIES must be an array of valid privacy policies", + ); + } else { + return parsedPolicies; + } + } catch (error) { + console.warn( + `Failed to parse SATP_PRIVACY_POLICIES: ${error.message}. Using default.`, + ); } + } + return []; + } - if (!pluginOptions.environment) { - pluginOptions.environment = "development"; + private static processMergePolicies(): IMergePolicyValue[] { + if (process.env.SATP_MERGE_POLICIES) { + try { + const parsedPolicies = JSON.parse( + process.env.SATP_MERGE_POLICIES, + ) as IMergePolicyValue[]; + + const validPolicies = ( + eachPolicy: unknown, + ): eachPolicy is IMergePolicyValue => { + if (!eachPolicy) { + return false; + } + const policy = eachPolicy as Record; + return ( + "policy" in policy && + "policyHash" in policy && + typeof policy.policy === "string" && + typeof policy.policyHash === "string" && + (policy.policy === MergePolicyOpts.PruneState || + policy.policy === MergePolicyOpts.PruneStateFromView || + policy.policy === MergePolicyOpts.NONE) + ); + }; + + if ( + !Array.isArray(parsedPolicies) || + !parsedPolicies.every(validPolicies) + ) { + throw new Error( + "SATP_MERGE_POLICIES must be an array of valid merge policies if provided", + ); + } else { + return parsedPolicies; + } + } catch (error) { + console.warn( + `Failed to parse SATP_MERGE_POLICIES: ${error.message}. Using default.`, + ); } + } + return []; + } - if (!pluginOptions.enableOpenAPI) { - pluginOptions.enableOpenAPI = true; - } + static ProcessGatewayCoordinatorConfig( + pluginOptions: SATPGatewayConfig, + ): SATPGatewayConfig { + if (!pluginOptions.keyPair) { + pluginOptions.keyPair = this.processKeyPair(); + } - if (!pluginOptions.validationOptions) { - // do nothing - } + pluginOptions.gid = this.processGatewayIdentity(pluginOptions); + + if (!pluginOptions.counterPartyGateways) { + pluginOptions.counterPartyGateways = this.processCounterPartyGateways(); + } + + if (!pluginOptions.logLevel) { + pluginOptions.logLevel = this.processLogLevel(); + } + + if (!pluginOptions.environment) { + pluginOptions.environment = this.processEnvironment(); + } + + if (!pluginOptions.enableOpenAPI) { + pluginOptions.enableOpenAPI = this.processEnableOpenAPI(); + } + + if (!pluginOptions.validationOptions) { + pluginOptions.validationOptions = this.processValidationOptions(); + } + + if (!pluginOptions.privacyPolicies) { + pluginOptions.privacyPolicies = this.processPrivacyPolicies(); } + + if (!pluginOptions.mergePolicies) { + pluginOptions.mergePolicies = this.processMergePolicies(); + } + return pluginOptions; } diff --git a/packages/cactus-plugin-satp-hermes/src/test/typescript/integration/gateway-init-startup.test.ts b/packages/cactus-plugin-satp-hermes/src/test/typescript/integration/gateway-init-startup.test.ts index b4b9b8355c..7306c03200 100644 --- a/packages/cactus-plugin-satp-hermes/src/test/typescript/integration/gateway-init-startup.test.ts +++ b/packages/cactus-plugin-satp-hermes/src/test/typescript/integration/gateway-init-startup.test.ts @@ -281,7 +281,8 @@ describe("SATPGateway startup", () => { ], supportedDLTs: [SupportedChain.FABRIC, SupportedChain.BESU], proofID: "mockProofID10", - gatewayClientPort: 3010, + gatewayServerPort: 13010, + gatewayClientPort: 13011, address: "https://localhost", }, }; @@ -289,7 +290,8 @@ describe("SATPGateway startup", () => { expect(gateway).toBeInstanceOf(SATPGateway); const identity = gateway.Identity; - expect(identity.gatewayClientPort).toBe(3010); + expect(identity.gatewayServerPort).toBe(13010); + expect(identity.gatewayClientPort).toBe(13011); expect(identity.address).toBe("https://localhost"); await gateway.startup(); await gateway.shutdown(); diff --git a/packages/cactus-plugin-satp-hermes/src/test/typescript/unit/constructor-instantiation.test.ts b/packages/cactus-plugin-satp-hermes/src/test/typescript/unit/constructor-instantiation.test.ts new file mode 100644 index 0000000000..74063fb942 --- /dev/null +++ b/packages/cactus-plugin-satp-hermes/src/test/typescript/unit/constructor-instantiation.test.ts @@ -0,0 +1,58 @@ +import "jest-extended"; +import { LogLevelDesc } from "@hyperledger/cactus-common"; +import { + ISATPGatewayRunnerConstructorOptions, + pruneDockerAllIfGithubAction, + SATPGatewayRunner, +} from "@hyperledger/cactus-test-tooling"; +import { + DEFAULT_PORT_GATEWAY_CLIENT, + DEFAULT_PORT_GATEWAY_SERVER, +} from "../../../main/typescript/core/constants"; + +const testCase = "Instantiate SATP Gateway Runner"; +const logLevel: LogLevelDesc = "TRACE"; + +describe(testCase, () => { + let gatewayRunner: SATPGatewayRunner; + + const gatewayRunnerOptions: ISATPGatewayRunnerConstructorOptions = { + containerImageVersion: "2024-09-02-3117", + containerImageName: + "ghcr.io/brunoffmateus/cactus-plugin-satp-hermes-gateway", + serverPort: DEFAULT_PORT_GATEWAY_SERVER, + clientPort: DEFAULT_PORT_GATEWAY_CLIENT, + logLevel, + emitContainerLogs: true, + }; + + beforeAll(async () => { + const pruning = pruneDockerAllIfGithubAction({ logLevel }); + await expect(pruning).toResolve(); + }); + + afterAll(async () => { + await gatewayRunner.stop(); + await gatewayRunner.destroy(); + await pruneDockerAllIfGithubAction({ logLevel }); + }); + + test(testCase, async () => { + gatewayRunner = new SATPGatewayRunner(gatewayRunnerOptions); + await gatewayRunner.start(); + expect(gatewayRunner).toBeTruthy(); + expect(gatewayRunner.getContainer()).toBeTruthy(); + + await expect( + gatewayRunner.waitForHealthCheck(60000), + ).resolves.not.toThrow(); + + const serverHost = await gatewayRunner.getServerHost(); + expect(serverHost).toBeTruthy(); + expect(serverHost).toMatch(/^http:\/\/localhost:\d+$/); + + const clientHost = await gatewayRunner.getClientHost(); + expect(clientHost).toBeTruthy(); + expect(clientHost).toMatch(/^http:\/\/localhost:\d+$/); + }); +}); diff --git a/packages/cactus-plugin-satp-hermes/supervisord.conf b/packages/cactus-plugin-satp-hermes/supervisord.conf new file mode 100644 index 0000000000..8ec4de113e --- /dev/null +++ b/packages/cactus-plugin-satp-hermes/supervisord.conf @@ -0,0 +1,19 @@ +[supervisord] +logfile=/usr/src/app/cactus/log/supervisord.log +logfile_maxbytes=50MB +logfile_backups=10 +loglevel=info +user=root + +[program:satp-gateway] +command=/run-satp-gateway.sh +autostart=true +autorestart=unexpected +exitcodes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 + +# [inet_http_server] +# port = 0.0.0.0:9001 \ No newline at end of file diff --git a/packages/cactus-test-tooling/src/main/typescript/public-api.ts b/packages/cactus-test-tooling/src/main/typescript/public-api.ts index ec19ee7324..85ddb9f322 100755 --- a/packages/cactus-test-tooling/src/main/typescript/public-api.ts +++ b/packages/cactus-test-tooling/src/main/typescript/public-api.ts @@ -8,6 +8,11 @@ export { BESU_TEST_LEDGER_OPTIONS_JOI_SCHEMA, } from "./besu/besu-test-ledger"; +export { + SATPGatewayRunner, + ISATPGatewayRunnerConstructorOptions, +} from "./satp/satp-gateway-runner"; + export { BesuMpTestLedger, IBesuMpTestLedgerOptions, diff --git a/packages/cactus-test-tooling/src/main/typescript/satp/satp-gateway-runner.ts b/packages/cactus-test-tooling/src/main/typescript/satp/satp-gateway-runner.ts new file mode 100644 index 0000000000..b4094d9198 --- /dev/null +++ b/packages/cactus-test-tooling/src/main/typescript/satp/satp-gateway-runner.ts @@ -0,0 +1,302 @@ +import Docker, { Container, ContainerInfo } from "dockerode"; +import Joi from "joi"; +import { EventEmitter } from "events"; +import { + LogLevelDesc, + Logger, + LoggerProvider, + Bools, +} from "@hyperledger/cactus-common"; +import { ITestLedger } from "../i-test-ledger"; +import { Containers } from "../common/containers"; + +export interface ISATPGatewayRunnerConstructorOptions { + containerImageVersion?: string; + containerImageName?: string; + serverPort?: number; + clientPort?: number; + envVars?: string[]; + logLevel?: LogLevelDesc; + emitContainerLogs?: boolean; +} + +export const SATP_GATEWAY_RUNNER_DEFAULT_OPTIONS = Object.freeze({ + containerImageVersion: "2024-09-02-3117", + containerImageName: "ghcr.io/brunoffmateus/cactus-plugin-satp-hermes-gateway", + serverPort: 3010, + clientPort: 3011, + envVars: [ + "SATP_GATEWAY_ID=gateway", + "SATP_GATEWAY_NAME=ExampleGateway", + "SATP_SUPPORTED_DLTS=FabricSATPGateway,BesuSATPGateway", + "SATP_GATEWAY_ADDRESS=http://localhost", + "SATP_PROOF_ID=mockProofID1", + 'SATP_COUNTER_PARTY_GATEWAYS=[{"id":"gateway1","version":"v02,v02,v02","supportedDLTs":"FabricSATPGateway,BesuSATPGateway"}]', + ], +}); + +export const SATP_GATEWAY_RUNNER_OPTIONS_JOI_SCHEMA: Joi.Schema = + Joi.object().keys({ + containerImageVersion: Joi.string().min(1).required(), + containerImageName: Joi.string().min(1).required(), + serverPort: Joi.number() + .integer() + .positive() + .min(1024) + .max(65535) + .required(), + clientPort: Joi.number() + .integer() + .positive() + .min(1024) + .max(65535) + .required(), + envVars: Joi.array().items(Joi.string()).required(), + }); + +export class SATPGatewayRunner implements ITestLedger { + public readonly containerImageVersion: string; + public readonly containerImageName: string; + public readonly serverPort: number; + public readonly clientPort: number; + public readonly envVars: string[]; + public readonly emitContainerLogs: boolean; + + private readonly log: Logger; + private container: Container | undefined; + private containerId: string | undefined; + + constructor( + public readonly options: ISATPGatewayRunnerConstructorOptions = {}, + ) { + if (!options) { + throw new TypeError(`SATPGatewayRunner#ctor options was falsy.`); + } + this.containerImageVersion = + options.containerImageVersion || + SATP_GATEWAY_RUNNER_DEFAULT_OPTIONS.containerImageVersion; + this.containerImageName = + options.containerImageName || + SATP_GATEWAY_RUNNER_DEFAULT_OPTIONS.containerImageName; + this.serverPort = + options.serverPort || SATP_GATEWAY_RUNNER_DEFAULT_OPTIONS.serverPort; + this.clientPort = + options.clientPort || SATP_GATEWAY_RUNNER_DEFAULT_OPTIONS.clientPort; + this.envVars = + options.envVars || SATP_GATEWAY_RUNNER_DEFAULT_OPTIONS.envVars; + + this.emitContainerLogs = Bools.isBooleanStrict(options.emitContainerLogs) + ? (options.emitContainerLogs as boolean) + : true; + + this.validateConstructorOptions(); + const label = "satp-gateway-runner"; + const level = options.logLevel || "INFO"; + this.log = LoggerProvider.getOrCreate({ level, label }); + } + + public getContainer(): Container { + const fnTag = "SATPGatewayRunner#getContainer()"; + if (!this.container) { + throw new Error(`${fnTag} container not yet started by this instance.`); + } else { + return this.container; + } + } + + public getContainerImageName(): string { + return `${this.containerImageName}:${this.containerImageVersion}`; + } + + public async getServerHost(): Promise { + const containerInfo = await this.getContainerInfo(); + const hostPort = await Containers.getPublicPort( + this.serverPort, + containerInfo, + ); + return `http://localhost:${hostPort}`; + } + + public async getClientHost(): Promise { + const containerInfo = await this.getContainerInfo(); + const hostPort = await Containers.getPublicPort( + this.clientPort, + containerInfo, + ); + return `http://localhost:${hostPort}`; + } + + public async start(omitPull = false): Promise { + const imageFqn = this.getContainerImageName(); + + if (this.container) { + await this.container.stop(); + await this.container.remove(); + } + const docker = new Docker(); + + if (!omitPull) { + this.log.debug(`Pulling container image ${imageFqn} ...`); + await this.pullContainerImage(imageFqn); + this.log.debug(`Pulled ${imageFqn} OK. Starting container...`); + } + + return new Promise((resolve, reject) => { + const eventEmitter: EventEmitter = docker.run( + imageFqn, + [], + [], + { + ExposedPorts: { + "3010/tcp": {}, // SERVER_PORT + "3011/tcp": {}, // CLIENT_PORT + }, + Healthcheck: { + Test: [ + "CMD-SHELL", + `curl -f http://localhost:3010/health && \ + curl -f http://localhost:3011/health`, + ], + Interval: 1000000000, // 1 second + Timeout: 3000000000, // 3 seconds + Retries: 99, + StartPeriod: 10000000000, // 10 seconds + }, + HostConfig: { + PublishAllPorts: true, + }, + Env: this.envVars, + }, + {}, + (err: unknown) => { + if (err) { + reject(err); + } + }, + ); + + eventEmitter.once("start", async (container: Container) => { + this.log.debug(`Started container OK. Waiting for healthcheck...`); + this.container = container; + this.containerId = container.id; + + if (this.emitContainerLogs) { + const fnTag = `[${this.getContainerImageName()}]`; + await Containers.streamLogs({ + container: this.getContainer(), + tag: fnTag, + log: this.log, + }); + } + + try { + await this.waitForHealthCheck(); + this.log.debug(`Healthcheck passing OK.`); + resolve(container); + } catch (ex) { + reject(ex); + } + }); + }); + } + + public async waitForHealthCheck(timeoutMs = 360000): Promise { + const fnTag = "SATPGatewayRunner#waitForHealthCheck()"; + const startedAt = Date.now(); + let isHealthy = false; + do { + if (Date.now() >= startedAt + timeoutMs) { + throw new Error(`${fnTag} timed out (${timeoutMs}ms)`); + } + const { Status, State } = await this.getContainerInfo(); + this.log.debug(`ContainerInfo.Status=%o, State=O%`, Status, State); + isHealthy = Status.endsWith("(healthy)"); + if (!isHealthy) { + await new Promise((resolve2) => setTimeout(resolve2, 1000)); + } + } while (!isHealthy); + } + + public stop(): Promise { + const fnTag = "SATPGatewayRunner#stop()"; + return new Promise((resolve, reject) => { + if (this.container) { + this.container.stop({}, (err: unknown, result: unknown) => { + if (err) { + reject(err); + } else { + resolve(result); + } + }); + } else { + return reject(new Error(`${fnTag} Container was not running.`)); + } + }); + } + + public destroy(): Promise { + const fnTag = "SATPGatewayRunner#destroy()"; + if (this.container) { + return this.container.remove(); + } else { + const ex = new Error(`${fnTag} Container not found, nothing to destroy.`); + return Promise.reject(ex); + } + } + + public async getContainerInfo(): Promise { + const docker = new Docker(); + const containerInfos = await docker.listContainers({}); + + let aContainerInfo; + if (this.containerId !== undefined) { + aContainerInfo = containerInfos.find((ci) => ci.Id === this.containerId); + } + + if (aContainerInfo) { + return aContainerInfo; + } else { + throw new Error( + `SATPGatewayRunner#getContainerInfo() no container with ID "${this.containerId}"`, + ); + } + } + + private pullContainerImage(containerNameAndTag: string): Promise { + return new Promise((resolve, reject) => { + const docker = new Docker(); + docker.pull(containerNameAndTag, (pullError: unknown, stream: never) => { + if (pullError) { + reject(pullError); + } else { + docker.modem.followProgress( + stream, + (progressError: unknown, output: unknown[]) => { + if (progressError) { + reject(progressError); + } else { + resolve(output); + } + }, + ); + } + }); + }); + } + + private validateConstructorOptions(): void { + const validationResult = SATP_GATEWAY_RUNNER_OPTIONS_JOI_SCHEMA.validate({ + containerImageVersion: this.containerImageVersion, + containerImageName: this.containerImageName, + serverPort: this.serverPort, + clientPort: this.clientPort, + envVars: this.envVars, + }); + + if (validationResult.error) { + throw new Error( + `SATPGatewayRunner#ctor ${validationResult.error.annotate()}`, + ); + } + } +} diff --git a/yarn.lock b/yarn.lock index 11a8959a0b..44f0be8c56 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10844,6 +10844,7 @@ __metadata: sqlite3: "npm:5.1.5" swagger-cli: "npm:4.0.4" swagger-ui-express: "npm:5.0.0" + ts-node: "npm:10.9.1" typescript: "npm:5.5.2" typescript-optional: "npm:2.0.1" uuid: "npm:10.0.0"