├── .nvmrc ├── .prettierignore ├── .eslintignore ├── .dockerignore ├── jest.config.js ├── .gitignore ├── .prettierrc.js ├── Dockerfile ├── rollup.config.js ├── src ├── TransactionResult.ts ├── Authorization.ts ├── IdentityCache.ts ├── TransactionResult.test.ts ├── ChaincodeRequest.ts ├── config.ts ├── DiscoveryService.ts ├── NetworkPool.ts └── index.ts ├── tsconfig.json ├── test-network ├── chaincode-kv-node │ ├── package.json │ ├── index.js │ ├── index.spec.js │ └── package-lock.json ├── fablo-config.json ├── fablo-config-tls.json └── fablo ├── .github └── workflows │ ├── test.yml │ └── publish-docker.yml ├── .eslintrc.js ├── docker-build.sh ├── e2e ├── invoke.test.ts ├── run-e2e-tests-on-dist.sh ├── privateData.test.ts ├── happyPath.test.ts ├── testUtils.ts ├── authorization.test.ts └── discover.test.ts ├── package.json ├── README.md └── LICENSE /.nvmrc: -------------------------------------------------------------------------------- 1 | 18 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | }; 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | dist 3 | node_modules 4 | test-network/chaincode-kv-node/node_modules 5 | test-network/fablo-target 6 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | trailingComma: "all", 4 | singleQuote: false, 5 | printWidth: 120, 6 | tabWidth: 2 7 | }; -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20 2 | 3 | # Create app directory 4 | WORKDIR /usr/src/app 5 | 6 | # Install app dependencies 7 | # A wildcard is used to ensure both package.json AND package-lock.json are copied 8 | # where available (npm@5+) 9 | COPY package*.json ./ 10 | RUN npm ci --only=production 11 | 12 | # Copy app dist 13 | COPY dist . 14 | 15 | CMD [ "node", "index.js" ] 16 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import commonjs from "@rollup/plugin-commonjs"; 2 | import json from "@rollup/plugin-json"; 3 | import nodeResolve from "@rollup/plugin-node-resolve"; 4 | import typescript from "@rollup/plugin-typescript"; 5 | import pkg from "./package.json"; 6 | 7 | export default { 8 | input: pkg.main, 9 | output: [ 10 | { 11 | file: pkg.module, 12 | format: "es", 13 | sourcemap: true, 14 | globals: {}, 15 | }, 16 | ], 17 | 18 | plugins: [commonjs(), json(), nodeResolve(), typescript()], 19 | }; 20 | -------------------------------------------------------------------------------- /src/TransactionResult.ts: -------------------------------------------------------------------------------- 1 | import matches from "ts-matches"; 2 | 3 | const payloadWithStatusShape = matches.shape({ status: matches.natural, payload: matches.any }); 4 | 5 | export default { 6 | parse: (b: Buffer): { status: number; response: any } => { 7 | try { 8 | const payload: Record = JSON.parse(b.toString()); 9 | if (payloadWithStatusShape.test(payload)) { 10 | return { status: payload.status, response: payload.payload }; 11 | } else { 12 | return { status: 200, response: payload }; 13 | } 14 | } catch (_e) { 15 | return { status: 200, response: b.toString() }; 16 | } 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": "./src", 4 | "outDir": "./dist", 5 | "module": "commonjs", 6 | "target": "es6", 7 | "esModuleInterop": true, 8 | "allowJs": true, 9 | "declaration": true, 10 | "strict": true, 11 | "noUnusedLocals": true, 12 | "noUnusedParameters": true, 13 | "skipLibCheck": true, 14 | "moduleResolution": "node", 15 | "lib": [ 16 | "ES5", 17 | "ES2015", 18 | "ES2015.Promise" 19 | ] 20 | }, 21 | "include": [ 22 | "src/**/*" 23 | ], 24 | "exclude": [ 25 | "node_modules", 26 | "dist", 27 | "src/**/*.test.ts", 28 | "e2e" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /test-network/chaincode-kv-node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chaincode-kv-node", 3 | "version": "0.2.0", 4 | "main": "index.js", 5 | "engines": { 6 | "node": ">=8", 7 | "npm": ">=5" 8 | }, 9 | "scripts": { 10 | "start": "fabric-chaincode-node start", 11 | "start:dev": "fabric-chaincode-node start --peer.address \"127.0.0.1:8541\" --chaincode-id-name \"chaincode1:0.0.1\" --tls.enabled false", 12 | "start:watch": "nodemon --exec \"npm run start:dev\"", 13 | "build": "echo \"No need to build the chaincode\"", 14 | "lint": "eslint . --fix --ext .js" 15 | }, 16 | "author": "SoftwareMill", 17 | "dependencies": { 18 | "fabric-contract-api": "2.4.2", 19 | "fabric-shim": "2.4.2" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: [ push ] 3 | jobs: 4 | Test-Actions: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - name: Check out repository code 8 | uses: actions/checkout@v2 9 | 10 | - name: Build fablo-rest 11 | run: npm install && npm run lint && npm run build && ls -lh ./dist 12 | 13 | - name: Run unit tests 14 | run: npm run test 15 | 16 | - name: Build fablo-rest Docker image 17 | run: ./docker-build.sh 18 | 19 | - name: Start test network 20 | run: (ls -lh && cd ./test-network && ./fablo up fablo-config-tls.json) 21 | 22 | - name: Start fablo-rest as docker container and run e2e tests 23 | run: ./e2e/run-e2e-tests-on-dist.sh 24 | 25 | - uses: actions/upload-artifact@v4 26 | if: failure() 27 | with: 28 | name: server.log 29 | path: server.log 30 | -------------------------------------------------------------------------------- /src/Authorization.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import IdentityCache, { CachedIdentity } from "./IdentityCache"; 3 | 4 | type IdentityWithToken = CachedIdentity & { token: string }; 5 | 6 | const getFromToken = async ( 7 | request: express.Request, 8 | response: express.Response, 9 | ): Promise => { 10 | const authToken = request.token; 11 | if (!authToken) { 12 | const message = "Missing authorization header"; 13 | response.status(400).send({ message }); 14 | return undefined; 15 | } 16 | 17 | const identity = await IdentityCache.get(authToken); 18 | if (!identity) { 19 | const message = "User with provided token is not enrolled"; 20 | response.status(403).send({ message }); 21 | return undefined; 22 | } 23 | 24 | return { ...identity, token: authToken }; 25 | }; 26 | 27 | export default { 28 | getFromToken, 29 | }; 30 | -------------------------------------------------------------------------------- /.github/workflows/publish-docker.yml: -------------------------------------------------------------------------------- 1 | name: Publish docker image 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | types: [closed] 8 | 9 | env: 10 | IMAGE_NAME: ghcr.io/fablo-io/fablo-rest 11 | 12 | jobs: 13 | publish-docker-image: 14 | if: github.event.pull_request.merged == true 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Check out repository code 18 | uses: actions/checkout@v2 19 | 20 | - name: Set up Docker Buildx 21 | uses: docker/setup-buildx-action@v3 22 | with: 23 | driver: docker-container 24 | 25 | - name: Log in to GitHub Container Registry 26 | uses: docker/login-action@v3 27 | with: 28 | registry: ghcr.io 29 | username: ${{ github.actor }} 30 | password: ${{ secrets.GITHUB_TOKEN }} 31 | 32 | - name: Build and push Docker image 33 | run: | 34 | ./docker-build.sh --push -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | "plugin:@typescript-eslint/recommended", // Uses the recommended rules from the @typescript-eslint/eslint-plugin 4 | "prettier/@typescript-eslint", // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier 5 | "plugin:prettier/recommended", // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array. 6 | ], 7 | parser: "@typescript-eslint/parser", 8 | plugins: ["@typescript-eslint/eslint-plugin", "prettier"], 9 | settings: { 10 | "import/parsers": { 11 | "@typescript-eslint/parser": [".ts", ".tsx"], 12 | }, 13 | "import/resolver": { 14 | typescript: {}, 15 | }, 16 | }, 17 | rules: { 18 | "prettier/prettier": "error", 19 | "@typescript-eslint/no-explicit-any": "off", 20 | "@typescript-eslint/no-use-before-define": "error", 21 | "react/display-name": ["off"], 22 | "react/prop-types": ["off"], 23 | "@typescript-eslint/explicit-function-return-type": ["off"], 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /src/IdentityCache.ts: -------------------------------------------------------------------------------- 1 | import * as uuid from "uuid"; 2 | import NodeCache from "node-cache"; 3 | import { ICryptoKey, User } from "fabric-common"; 4 | 5 | const cache = new NodeCache({ stdTTL: 60 * 10, useClones: false }); 6 | 7 | interface StoredIdentity { 8 | credentials: { 9 | certificate: string; 10 | privateKey: string; 11 | }; 12 | mspId: string; 13 | type: "X.509"; 14 | } 15 | 16 | export interface CachedIdentity { 17 | identity: StoredIdentity; 18 | user: User; 19 | } 20 | 21 | const put = async (name: string, privateKey: ICryptoKey, certificate: string, mspId: string): Promise => { 22 | const key = uuid.v1() + "-" + name; 23 | 24 | const identity: StoredIdentity = { 25 | credentials: { 26 | certificate, 27 | privateKey: privateKey.toBytes(), 28 | }, 29 | mspId, 30 | type: "X.509", 31 | }; 32 | 33 | const user = new User(name); 34 | await user.setEnrollment(privateKey, certificate, mspId); 35 | 36 | cache.set(key, { identity, user }); 37 | return key; 38 | }; 39 | 40 | const del = (key: string): number => cache.del(key); 41 | 42 | const get = async (key: string): Promise => cache.get(key); 43 | 44 | export default { put, get, del }; 45 | -------------------------------------------------------------------------------- /docker-build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo 4 | 5 | FABLO_REST_HOME="$(cd "$(dirname "$0")" && pwd)" 6 | FABLO_REST_VERSION=$(jq -r '.version' <"$FABLO_REST_HOME/package.json") 7 | DOCKER_IMAGE_BASE_NAME="ghcr.io/fablo-io/fablo-rest" 8 | DOCKER_IMAGE_TAG="$DOCKER_IMAGE_BASE_NAME:$FABLO_REST_VERSION" 9 | 10 | COMMIT_HASH=$(git rev-parse --short HEAD) 11 | BUILD_DATE=$(date +'%Y-%m-%d-%H:%M:%S') 12 | VERSION_DETAILS="$BUILD_DATE-$COMMIT_HASH" 13 | 14 | echo "Building new image..." 15 | echo " FABLO_REST_HOME: $FABLO_REST_HOME" 16 | echo " FABLO_REST_VERSION: $FABLO_REST_VERSION" 17 | echo " DOCKER_IMAGE_TAG: [$DOCKER_IMAGE_BASE_NAME, $DOCKER_IMAGE_TAG]" 18 | echo " VERSION_DETAILS: $VERSION_DETAILS" 19 | 20 | npm install --silent 21 | npm run build 22 | 23 | # if --push is passed, then build for all platforms and push the image to the registry 24 | if [ "${1:-''}" = "--push" ]; then 25 | docker buildx build \ 26 | --build-arg VERSION_DETAILS="$VERSION_DETAILS" \ 27 | --platform linux/amd64,linux/arm64 \ 28 | --tag "$DOCKER_IMAGE_TAG" \ 29 | --tag "$DOCKER_IMAGE_BASE_NAME:latest" \ 30 | --push \ 31 | "$FABLO_REST_HOME" 32 | else 33 | docker buildx build \ 34 | --build-arg VERSION_DETAILS="$VERSION_DETAILS" \ 35 | --platform linux/amd64 \ 36 | --tag "$DOCKER_IMAGE_TAG" \ 37 | --tag "$DOCKER_IMAGE_BASE_NAME:latest" \ 38 | "$FABLO_REST_HOME" 39 | fi 40 | 41 | -------------------------------------------------------------------------------- /src/TransactionResult.test.ts: -------------------------------------------------------------------------------- 1 | import TransactionResult from "./TransactionResult"; 2 | 3 | describe("TransactionResult.parse", () => { 4 | const helloMessage = "Hello from chaincode"; 5 | const helloNumber = 42; 6 | const helloJson = { message: helloMessage, count: helloNumber }; 7 | 8 | it("should parse a string", () => { 9 | // Given 10 | const response = Buffer.from(helloMessage); 11 | 12 | // When 13 | const parsed = TransactionResult.parse(response); 14 | 15 | // Then 16 | expect(parsed).toEqual({ status: 200, response: helloMessage }); 17 | }); 18 | 19 | it("should parse a valid JSON", () => { 20 | // Given 21 | const response = Buffer.from(JSON.stringify(helloJson)); 22 | 23 | // When 24 | const parsed = TransactionResult.parse(response); 25 | 26 | // Then 27 | expect(parsed).toEqual({ status: 200, response: helloJson }); 28 | }); 29 | 30 | it("should parse a JSON that conforms recommended response shape", () => { 31 | // Given 32 | const status = 201; 33 | const response = Buffer.from(JSON.stringify({ status, payload: helloJson })); 34 | 35 | // When 36 | const parsed = TransactionResult.parse(response); 37 | 38 | // Then 39 | expect(parsed).toEqual({ status, response: helloJson }); 40 | }); 41 | 42 | it("should return valid response for empty result", () => { 43 | // Given 44 | const response = Buffer.alloc(0); 45 | 46 | // When 47 | const parsed = TransactionResult.parse(response); 48 | 49 | // Then 50 | expect(parsed).toEqual({ status: 200, response: "" }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /e2e/invoke.test.ts: -------------------------------------------------------------------------------- 1 | import * as uuid from "uuid"; 2 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 3 | // @ts-ignore 4 | import { callDefaultChaincode, generateEnrolledUser } from "./testUtils"; 5 | 6 | const invokePut = (token: string, arg1: string, arg2: string) => 7 | callDefaultChaincode(token, "invoke", "KVContract:put", [arg1, arg2]); 8 | 9 | const invokeGet = (token: string, arg1: string) => callDefaultChaincode(token, "invoke", "KVContract:get", [arg1]); 10 | 11 | const invokeHistory = (token: string, arg1: string) => 12 | callDefaultChaincode(token, "invoke", "KVContract:getHistory", [arg1]); 13 | 14 | jest.setTimeout(10000); 15 | 16 | describe("Invoke", () => { 17 | // todo: this test fails sometimes (to be resolved) 18 | it.skip("invoke concurrently (MVCC failure)", async () => { 19 | // Given 20 | const { token } = await generateEnrolledUser(); 21 | const key = `key-${uuid.v1()}`; 22 | const initialInvokeResponse = await invokeGet(token, key); // why? to cache the discovery results 23 | expect(initialInvokeResponse).toEqual({ status: 200, body: { response: { error: "NOT_FOUND" } } }); 24 | 25 | // When 26 | const invokeResponses = await Promise.all([ 27 | invokePut(token, key, "Hello Alice"), 28 | invokeGet(token, key), // note: two puts succeeds regardless of arg2 value 29 | invokePut(token, key, "Hello Bob"), 30 | invokeGet(token, key), // note: two puts succeeds regardless of arg2 value 31 | invokePut(token, key, "Hello Carol"), 32 | ]); 33 | 34 | const historyResult = (await invokeHistory(token, key)).body.response as unknown[]; 35 | 36 | // Then 37 | console.log(JSON.stringify(invokeResponses, undefined, 2)); 38 | console.log(JSON.stringify(historyResult, undefined, 2)); 39 | expect(invokeResponses.find((r) => r.status === 400)?.body).toEqual({ message: "MVCC_READ_CONFLICT" }); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /e2e/run-e2e-tests-on-dist.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # 4 | # You need to run './fablo up' before this script to have running Hyperledger Fabric network 5 | # and execute ./docker-build.sh script to build fablo-rest Docker image. 6 | # 7 | set -e 8 | 9 | FABLO_REST_HOME="$(cd "$(dirname "$0")/.." && pwd)" 10 | cd "$FABLO_REST_HOME" 11 | 12 | # 13 | # Get network name from fablo-target 14 | # 15 | network_name="$(cat "$FABLO_REST_HOME/test-network/fablo-target/fabric-docker/.env" | grep "COMPOSE_PROJECT_NAME=")" 16 | network_name="${network_name#*=}_basic" 17 | echo "Network name: $network_name" 18 | 19 | # 20 | # Start application in the container. 21 | # 22 | container=fablo_rest_test 23 | port=8000 24 | discovery_urls="grpcs://peer0.org2.com:7061,grpcs://wrong.org2.com:9999,grpcs://peer1.org2.com:7062" 25 | discovery_tls_ca_cert_files="/peer-crypto/org2.com/peers/peer0.org2.com/tls/ca.crt,/peer-crypto/org2.com/peers/peer0.org2.com/tls/ca.crt,/peer-crypto/org2.com/peers/peer1.org2.com/tls/ca.crt" 26 | 27 | docker run \ 28 | -e PORT=9999 \ 29 | -e MSP_ID="Org2MSP" \ 30 | -e FABRIC_CA_URL="https://ca.org2.com:7054" \ 31 | -e FABRIC_CA_NAME="ca.org2.com" \ 32 | -e DISCOVERY_URLS="$discovery_urls" \ 33 | -e DISCOVERY_TLS_CA_CERT_FILES="$discovery_tls_ca_cert_files" \ 34 | -e AS_LOCALHOST="false" \ 35 | -p "$port:9999" \ 36 | --network="$network_name" \ 37 | -v "$FABLO_REST_HOME/test-network/fablo-target/fabric-config/crypto-config/peerOrganizations:/peer-crypto"\ 38 | -d \ 39 | --rm \ 40 | --name "$container" \ 41 | ghcr.io/fablo-io/fablo-rest 42 | 43 | echo "Started server on container $container at port $port" 44 | trap "echo 'Stopping container $container' && docker stop $container" EXIT SIGINT ERR 45 | sleep 5 46 | 47 | # 48 | # Run tests 49 | # 50 | PORT=$port \ 51 | MSP_ID="Org2MSP" \ 52 | FABRIC_CA_NAME="ca.org2.com" \ 53 | DISCOVERY_URLS="$discovery_urls" \ 54 | npm run test-e2e 55 | -------------------------------------------------------------------------------- /test-network/chaincode-kv-node/index.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | const { Contract } = require("fabric-contract-api"); 3 | // eslint-disable-next-line @typescript-eslint/no-var-requires 4 | const crypto = require("crypto"); 5 | 6 | class KVContract extends Contract { 7 | constructor() { 8 | super("KVContract"); 9 | } 10 | 11 | async instantiate() { 12 | // function that will be invoked on chaincode instantiation 13 | } 14 | 15 | async put(ctx, key, value) { 16 | await ctx.stub.putState(key, Buffer.from(value)); 17 | return { success: "OK" }; 18 | } 19 | 20 | async get(ctx, key) { 21 | const buffer = await ctx.stub.getState(key); 22 | if (!buffer || !buffer.length) return { error: "NOT_FOUND" }; 23 | return { success: buffer.toString() }; 24 | } 25 | 26 | async putPrivateMessage(ctx, collection) { 27 | const transient = ctx.stub.getTransient(); 28 | const message = transient.get("message"); 29 | await ctx.stub.putPrivateData(collection, "message", message); 30 | return { success: "OK" }; 31 | } 32 | 33 | async getPrivateMessage(ctx, collection) { 34 | const message = await ctx.stub.getPrivateData(collection, "message"); 35 | const messageString = message.toBuffer ? message.toBuffer().toString() : message.toString(); 36 | return { success: messageString }; 37 | } 38 | 39 | async verifyPrivateMessage(ctx, collection) { 40 | const transient = ctx.stub.getTransient(); 41 | const message = transient.get("message"); 42 | const messageString = message.toBuffer ? message.toBuffer().toString() : message.toString(); 43 | const currentHash = crypto.createHash("sha256").update(messageString).digest("hex"); 44 | const privateDataHash = (await ctx.stub.getPrivateDataHash(collection, "message")).toString("hex"); 45 | if (privateDataHash !== currentHash) { 46 | return { error: "VERIFICATION_FAILED" }; 47 | } 48 | return { success: "OK" }; 49 | } 50 | } 51 | 52 | exports.contracts = [KVContract]; 53 | -------------------------------------------------------------------------------- /src/ChaincodeRequest.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import matches from "ts-matches"; 3 | 4 | interface ChaincodeRequestT { 5 | channelName: string; 6 | chaincodeName: string; 7 | method: string; 8 | args: string[]; 9 | transient: Record; 10 | } 11 | 12 | const getValidOrError = (request: express.Request): { error: string } | ChaincodeRequestT => { 13 | const channelName: string | undefined = request.params.channelName; 14 | if (!channelName) return { error: "Missing channel name in path" }; 15 | 16 | const chaincodeName: string | undefined = request.params.chaincodeName; 17 | if (!chaincodeName) return { error: "Missing chaincode name in path" }; 18 | 19 | const method: string | undefined = request.body.method; 20 | if (!method) return { error: "Missing chaincode method in request body" }; 21 | 22 | const argsMatcher = matches.arrayOf(matches.string); 23 | if (!argsMatcher.test(request.body.args)) return { error: "Invalid chaincode args. It must be an array of strings" }; 24 | const args: string[] = request.body.args; 25 | 26 | const transientMatcher = matches.dictionary([matches.string, matches.string]).optional(); 27 | if (!transientMatcher.test(request.body.transient)) 28 | return { error: "Invalid transient parameter. It must be an object with string keys and string values" }; 29 | 30 | const transientStrings: Record = request.body.transient ?? {}; 31 | const transient: Record = Object.keys(transientStrings) 32 | .map((k) => ({ [k]: Buffer.from(transientStrings[k]) })) 33 | .reduce((map, e) => ({ ...map, ...e }), {}); 34 | 35 | return { 36 | channelName, 37 | chaincodeName, 38 | method, 39 | args, 40 | transient, 41 | }; 42 | }; 43 | 44 | const isError = (reqOrError: { error: string } | ChaincodeRequestT): reqOrError is { error: string } => 45 | "error" in reqOrError; 46 | 47 | const getValid = (request: express.Request, response: express.Response): ChaincodeRequestT | undefined => { 48 | const chaincodeReqOrError = getValidOrError(request); 49 | 50 | if (isError(chaincodeReqOrError)) { 51 | response.status(400).send({ mesage: chaincodeReqOrError.error }); 52 | return undefined; 53 | } 54 | 55 | return chaincodeReqOrError; 56 | }; 57 | 58 | export default { 59 | getValid, 60 | }; 61 | -------------------------------------------------------------------------------- /e2e/privateData.test.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 2 | // @ts-ignore 3 | import { callDefaultChaincode, generateEnrolledUser } from "./testUtils"; 4 | 5 | jest.setTimeout(10000); 6 | 7 | describe("Private data scenario", () => { 8 | const credentials = generateEnrolledUser(); 9 | const collection = "private-collection"; 10 | 11 | it("Put transient message to private data", async () => { 12 | // Given 13 | const { token } = await credentials; 14 | const method = "KVContract:putPrivateMessage"; 15 | const transient = { message: "Top secret message" }; 16 | 17 | // When 18 | const response = await callDefaultChaincode(token, "invoke", method, [collection], transient); 19 | 20 | // Then 21 | expect(response).toEqual( 22 | expect.objectContaining({ 23 | status: 200, 24 | body: { response: { success: "OK" } }, 25 | }), 26 | ); 27 | }); 28 | 29 | it("Read private data", async () => { 30 | // Given 31 | const { token } = await credentials; 32 | const method = "KVContract:getPrivateMessage"; 33 | 34 | // When 35 | const response = await callDefaultChaincode(token, "query", method, [collection]); 36 | 37 | // Then 38 | expect(response).toEqual( 39 | expect.objectContaining({ 40 | status: 200, 41 | body: { response: { success: "Top secret message" } }, 42 | }), 43 | ); 44 | }); 45 | 46 | it("Verify private data", async () => { 47 | const { token } = await credentials; 48 | const method = "KVContract:verifyPrivateMessage"; 49 | const transient1 = { message: "Top secret message" }; 50 | const transient2 = { message: "Invalid secret message" }; 51 | 52 | // When 53 | const response1 = await callDefaultChaincode(token, "query", method, [collection], transient1); 54 | const response2 = await callDefaultChaincode(token, "query", method, [collection], transient2); 55 | 56 | // Then 57 | expect(response1).toEqual( 58 | expect.objectContaining({ 59 | status: 200, 60 | body: { response: { success: "OK" } }, 61 | }), 62 | ); 63 | expect(response2).toEqual( 64 | expect.objectContaining({ 65 | status: 200, 66 | body: { response: { error: "VERIFICATION_FAILED" } }, 67 | }), 68 | ); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import { Utils } from "fabric-common"; 3 | import { LoggerInstance } from "winston"; 4 | 5 | const logger = Utils.getLogger("FabloRestConfig"); 6 | 7 | const readCertFile = (path: string): string => { 8 | const buffer = fs.readFileSync(path); 9 | return buffer.toString(); 10 | }; 11 | 12 | export interface DiscovererConfig { 13 | url: string; 14 | pem?: string; 15 | "ssl-target-name-override"?: string; 16 | } 17 | 18 | const getDiscovererConfigs = (urls: string, sslNameOverrides: string, pemPaths: string): DiscovererConfig[] => { 19 | const splitToArray = (s: string) => (!s.length ? [] : s.split(",").map((s) => s.trim())); 20 | const urlArray = splitToArray(urls); 21 | const sslNameOverridesArray = splitToArray(sslNameOverrides); 22 | const pemArray = splitToArray(pemPaths); 23 | 24 | return urlArray.map((url, i) => { 25 | const name = sslNameOverridesArray[i]; 26 | const pem = pemArray[i]; 27 | logger.info(`Configuring discoverer (url=${url}, ssl-name-override=${name}, pem=${pem})`); 28 | return { 29 | url, 30 | "ssl-target-name-override": name && name.length ? name : undefined, 31 | pem: pem && pem.length ? readCertFile(pem) : undefined, 32 | }; 33 | }); 34 | }; 35 | 36 | const defaults: Record = { 37 | PORT: "8000", 38 | MSP_ID: "Org1MSP", 39 | FABRIC_CA_URL: "http://localhost:7040", 40 | FABRIC_CA_NAME: "ca.org1.com", 41 | AS_LOCALHOST: "true", 42 | DISCOVERY_URLS: "grpc://localhost:7041,grpc://localhost:7061", 43 | DISCOVERY_SSL_TARGET_NAME_OVERRIDES: "", 44 | DISCOVERY_TLS_CA_CERT_FILES: "", 45 | }; 46 | 47 | const getOrDefault = (key: string): string => { 48 | const fromEnv = process.env[key]; 49 | if (fromEnv) return fromEnv; 50 | 51 | const defaultValue = defaults[key]; 52 | logger.warn(`No environment variable ${key} declared, using default ${defaultValue}`); 53 | return defaultValue; 54 | }; 55 | 56 | export default { 57 | PORT: getOrDefault("PORT"), 58 | MSP_ID: getOrDefault("MSP_ID"), 59 | FABRIC_CA_URL: getOrDefault("FABRIC_CA_URL"), 60 | FABRIC_CA_NAME: getOrDefault("FABRIC_CA_NAME"), 61 | AS_LOCALHOST: getOrDefault("AS_LOCALHOST") === "true", 62 | 63 | discovererConfigs: getDiscovererConfigs( 64 | getOrDefault("DISCOVERY_URLS"), 65 | getOrDefault("DISCOVERY_SSL_TARGET_NAME_OVERRIDES"), 66 | getOrDefault("DISCOVERY_TLS_CA_CERT_FILES"), 67 | ), 68 | 69 | getLogger: (name: string): LoggerInstance => Utils.getLogger(name), 70 | }; 71 | -------------------------------------------------------------------------------- /test-network/fablo-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://github.com/hyperledger-labs/fablo/releases/download/1.2.0/schema.json", 3 | "global": { 4 | "fabricVersion": "2.5.12", 5 | "tls": false 6 | }, 7 | "orgs": [ 8 | { 9 | "organization": { 10 | "name": "Orderer", 11 | "domain": "root.com" 12 | }, 13 | "orderers": [ 14 | { 15 | "groupName": "group1", 16 | "type": "solo", 17 | "instances": 1 18 | } 19 | ] 20 | }, 21 | { 22 | "organization": { 23 | "name": "Org1", 24 | "domain": "org1.com" 25 | }, 26 | "peer": { 27 | "instances": 2, 28 | "anchorPeerInstances": 2 29 | } 30 | }, 31 | { 32 | "organization": { 33 | "name": "Org2", 34 | "domain": "org2.com" 35 | }, 36 | "peer": { 37 | "instances": 2, 38 | "anchorPeerInstances": 2 39 | } 40 | } 41 | ], 42 | "channels": [ 43 | { 44 | "name": "my-channel1", 45 | "orgs": [ 46 | { 47 | "name": "Org1", 48 | "peers": [ 49 | "peer0", 50 | "peer1" 51 | ] 52 | }, 53 | { 54 | "name": "Org2", 55 | "peers": [ 56 | "peer0", 57 | "peer1" 58 | ] 59 | } 60 | ] 61 | }, 62 | { 63 | "name": "my-channel2", 64 | "orgs": [ 65 | { 66 | "name": "Org1", 67 | "peers": [ 68 | "peer1" 69 | ] 70 | }, 71 | { 72 | "name": "Org2", 73 | "peers": [ 74 | "peer1" 75 | ] 76 | } 77 | ] 78 | } 79 | ], 80 | "chaincodes": [ 81 | { 82 | "name": "chaincode1", 83 | "version": "0.0.1", 84 | "lang": "node", 85 | "channel": "my-channel1", 86 | "endorsement": "OR('Org1MSP.member', 'Org2MSP.member')", 87 | "directory": "./chaincode-kv-node", 88 | "privateData": [ 89 | { 90 | "name": "private-collection", 91 | "orgNames": [ 92 | "Org1", 93 | "Org2" 94 | ] 95 | } 96 | ] 97 | } 98 | ], 99 | "hooks": { 100 | "postGenerate": "perl -i -pe 's/docker-compose/docker compose/g' \"./fablo-target/fabric-docker/commands-generated.sh\" && perl -i -pe 's/docker-compose/docker compose/g' \"./fablo-target/fabric-docker/snapshot-scripts.sh\"" 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /test-network/fablo-config-tls.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://github.com/hyperledger-labs/fablo/releases/download/1.2.0/schema.json", 3 | "global": { 4 | "fabricVersion": "2.5.12", 5 | "tls": true 6 | }, 7 | "orgs": [ 8 | { 9 | "organization": { 10 | "name": "Orderer", 11 | "domain": "root.com" 12 | }, 13 | "orderers": [ 14 | { 15 | "groupName": "group1", 16 | "type": "solo", 17 | "instances": 1 18 | } 19 | ] 20 | }, 21 | { 22 | "organization": { 23 | "name": "Org1", 24 | "domain": "org1.com" 25 | }, 26 | "peer": { 27 | "instances": 2, 28 | "anchorPeerInstances": 2 29 | } 30 | }, 31 | { 32 | "organization": { 33 | "name": "Org2", 34 | "domain": "org2.com" 35 | }, 36 | "peer": { 37 | "instances": 2, 38 | "anchorPeerInstances": 2 39 | } 40 | } 41 | ], 42 | "channels": [ 43 | { 44 | "name": "my-channel1", 45 | "orgs": [ 46 | { 47 | "name": "Org1", 48 | "peers": [ 49 | "peer0", 50 | "peer1" 51 | ] 52 | }, 53 | { 54 | "name": "Org2", 55 | "peers": [ 56 | "peer0", 57 | "peer1" 58 | ] 59 | } 60 | ] 61 | }, 62 | { 63 | "name": "my-channel2", 64 | "orgs": [ 65 | { 66 | "name": "Org1", 67 | "peers": [ 68 | "peer1" 69 | ] 70 | }, 71 | { 72 | "name": "Org2", 73 | "peers": [ 74 | "peer1" 75 | ] 76 | } 77 | ] 78 | } 79 | ], 80 | "chaincodes": [ 81 | { 82 | "name": "chaincode1", 83 | "version": "0.0.1", 84 | "lang": "node", 85 | "channel": "my-channel1", 86 | "endorsement": "OR('Org1MSP.member', 'Org2MSP.member')", 87 | "directory": "./chaincode-kv-node", 88 | "privateData": [ 89 | { 90 | "name": "private-collection", 91 | "orgNames": [ 92 | "Org1", 93 | "Org2" 94 | ] 95 | } 96 | ] 97 | } 98 | ], 99 | "hooks": { 100 | "postGenerate": "perl -i -pe 's/docker-compose/docker compose/g' \"./fablo-target/fabric-docker/commands-generated.sh\" && perl -i -pe 's/docker-compose/docker compose/g' \"./fablo-target/fabric-docker/snapshot-scripts.sh\"" 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fablo-rest", 3 | "version": "0.2.0", 4 | "description": "REST API client for Hyperledger Fabric CA and chaincodes", 5 | "author": { 6 | "name": "Jakub Dzikowski", 7 | "email": "jakub@dzikowski.online", 8 | "url": "https://dzikowski.online" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/fablo-io/fablo-rest.git" 13 | }, 14 | "keywords": [ 15 | "hyperledger fabric", 16 | "chaincode", 17 | "smart contract", 18 | "rest" 19 | ], 20 | "license": "UNLICENSED", 21 | "bugs": { 22 | "url": "https://github.com/fablo-io/fablo-rest/issues" 23 | }, 24 | "homepage": "https://github.com/fablo-io/fablo-rest#readme", 25 | "main": "src/index.ts", 26 | "module": "dist/index.js", 27 | "types": "dist/index.d.ts", 28 | "scripts": { 29 | "clean": "rimraf dist", 30 | "start": "HFC_LOGGING='{\"debug\":\"console\"}' nodemon src/index.ts", 31 | "build": "rollup -c && tsc", 32 | "start-dist": "NODE_ENV=production node dist/index.js", 33 | "lint": "eslint . --ext .js,.jsx,.ts,.tsx --fix", 34 | "test-e2e": "jest -i ./e2e", 35 | "test": "jest ./src", 36 | "test-once": "jest" 37 | }, 38 | "husky": { 39 | "hooks": { 40 | "pre-commit": "npm run lint --fix", 41 | "pre-push": "npm run build" 42 | } 43 | }, 44 | "devDependencies": { 45 | "@rollup/plugin-commonjs": "^17.1.0", 46 | "@rollup/plugin-json": "^4.1.0", 47 | "@rollup/plugin-node-resolve": "^11.1.1", 48 | "@rollup/plugin-typescript": "^8.1.1", 49 | "@types/cors": "^2.8.12", 50 | "@types/jest": "^26.0.20", 51 | "@types/node-fetch": "^2.5.8", 52 | "@typescript-eslint/eslint-plugin": "^4.14.2", 53 | "@typescript-eslint/parser": "^4.14.2", 54 | "eslint": "^7.19.0", 55 | "eslint-config-airbnb": "^18.2.1", 56 | "eslint-config-prettier": "^7.2.0", 57 | "eslint-import-resolver-typescript": "^2.3.0", 58 | "eslint-plugin-import": "^2.22.1", 59 | "eslint-plugin-json": "^2.1.2", 60 | "eslint-plugin-prettier": "^3.3.1", 61 | "husky": "^4.3.8", 62 | "jest": "^30.1.3", 63 | "node-fetch": "^2.6.1", 64 | "nodemon": "^2.0.7", 65 | "prettier": "^2.2.1", 66 | "rimraf": "^3.0.2", 67 | "rollup": "^2.38.4", 68 | "ts-jest": "^29.4.3", 69 | "ts-loader": "^8.0.14", 70 | "ts-node": "^9.1.1", 71 | "typescript": "^4.1.3" 72 | }, 73 | "dependencies": { 74 | "@grpc/grpc-js": "^1.9.9", 75 | "@hyperledger/fabric-gateway": "^1.8.0", 76 | "@types/express": "^4.17.13", 77 | "@types/lodash": "^4.14.168", 78 | "@types/uuid": "^8.3.0", 79 | "cors": "^2.8.5", 80 | "express": "^4.17.1", 81 | "express-bearer-token": "^3.0.0", 82 | "fabric-ca-client": "^2.2.8", 83 | "fabric-common": "^2.2.9", 84 | "lodash": "^4.17.21", 85 | "node-cache": "^5.1.2", 86 | "ts-matches": "^4.0.2", 87 | "uuid": "^8.3.2", 88 | "winston": "^2.4.5" 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/DiscoveryService.ts: -------------------------------------------------------------------------------- 1 | import NodeCache from "node-cache"; 2 | import { Client, DiscoveryService, User } from "fabric-common"; 3 | import config, { DiscovererConfig } from "./config"; 4 | 5 | const discoveryEndpointsIndexCache = new NodeCache({ stdTTL: 60 * 5, useClones: false }); 6 | 7 | const logger = config.getLogger("FabloRestDiscoveryService"); 8 | 9 | const { discovererConfigs } = config; 10 | 11 | const attemptDiscovery = async ( 12 | client: Client, 13 | channelName: string, 14 | user: User, 15 | discovererConfig: DiscovererConfig, 16 | ) => { 17 | const userId = user.getName(); 18 | const identityContext = client.newIdentityContext(user); 19 | const channel = client.getChannel(channelName); 20 | 21 | const endpoint = client.newEndpoint({ 22 | url: discovererConfig.url, 23 | "ssl-target-name-override": discovererConfig["ssl-target-name-override"], 24 | pem: discovererConfig.pem, 25 | }); 26 | 27 | const discoverer = client.newDiscoverer(`discoverer-${userId}`); 28 | await discoverer.connect(endpoint); 29 | 30 | const discovery = channel.newDiscoveryService(`discovery-service-${channelName}-${userId}`); 31 | discovery.build(identityContext); 32 | discovery.sign(identityContext); 33 | await discovery.send({ targets: [discoverer], asLocalhost: config.AS_LOCALHOST }); 34 | 35 | return discovery; 36 | }; 37 | 38 | const createConnectedDiscoveryService = async ( 39 | client: Client, 40 | channelName: string, 41 | user: User, 42 | preferredIndex?: number, 43 | attemptsLeft: number = discovererConfigs.length, 44 | ): Promise => { 45 | const index = preferredIndex ?? discoveryEndpointsIndexCache.get(channelName) ?? 0; 46 | const discovererConfig = discovererConfigs[index]; 47 | 48 | const loggerParams = `channel=${channelName}, endpoint=${discovererConfig.url}, atemptsLeft=${attemptsLeft}`; 49 | logger.debug(`Creating and connecting discovery service (${loggerParams})`); 50 | 51 | try { 52 | const discovery = await attemptDiscovery(client, channelName, user, discovererConfig); 53 | discoveryEndpointsIndexCache.set(channelName, index); 54 | logger.debug(`Default discovery endpoint set to ${discovererConfig.url} (${loggerParams})`); 55 | return discovery; 56 | } catch (e) { 57 | logger.debug(`Discovery failed (${loggerParams}, error=${e instanceof Error ? e.message : String(e)})`); 58 | 59 | if (attemptsLeft === 0) { 60 | logger.error(`Discovery failed and no more attempts left. Will return error response (channel=${channelName})`); 61 | throw new Error(`No available discoverers for channel ${channelName}`); 62 | } 63 | 64 | const nextIndex = (index + 1) % discovererConfigs.length; 65 | return await createConnectedDiscoveryService(client, channelName, user, nextIndex, attemptsLeft - 1); 66 | } 67 | }; 68 | 69 | const create = async (client: Client, channelName: string, user: User): Promise => 70 | createConnectedDiscoveryService(client, channelName, user); 71 | 72 | export default { 73 | create, 74 | }; 75 | -------------------------------------------------------------------------------- /e2e/happyPath.test.ts: -------------------------------------------------------------------------------- 1 | import * as uuid from "uuid"; 2 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 3 | // @ts-ignore 4 | import { authorizationHeader, post } from "./testUtils"; 5 | 6 | jest.setTimeout(10000); 7 | 8 | describe("Happy path", () => { 9 | let adminEnrollToken: string; 10 | let userEnrollToken: string; 11 | 12 | const adminCredentials = { id: "admin", secret: "adminpw" }; 13 | const userCredentials = { id: uuid.v1(), secret: "secrett123" }; 14 | 15 | const chaincodeArg1 = uuid.v1(); 16 | 17 | it("should enroll admin", async () => { 18 | // When 19 | const response = await post("/user/enroll", adminCredentials); 20 | 21 | // Then 22 | expect(response).toEqual( 23 | expect.objectContaining({ 24 | status: 200, 25 | body: { token: expect.stringMatching(/.*/) }, 26 | }), 27 | ); 28 | 29 | adminEnrollToken = response.body.token; 30 | }); 31 | 32 | it("should register new user", async () => { 33 | // When 34 | const response = await post("/user/register", userCredentials, authorizationHeader(adminEnrollToken)); 35 | 36 | // Then 37 | expect(response).toEqual( 38 | expect.objectContaining({ 39 | status: 201, 40 | body: { message: "ok" }, 41 | }), 42 | ); 43 | }); 44 | 45 | it("should enroll user", async () => { 46 | // When 47 | const response = await post("/user/enroll", userCredentials); 48 | 49 | // Then 50 | expect(response).toEqual( 51 | expect.objectContaining({ 52 | status: 200, 53 | body: { token: expect.stringMatching(/.*/) }, 54 | }), 55 | ); 56 | 57 | userEnrollToken = response.body.token; 58 | }); 59 | 60 | it("should allow to invoke chaincode", async () => { 61 | // Given 62 | const channelName = "my-channel1"; 63 | const chaincodeName = "chaincode1"; 64 | const method = "KVContract:put"; 65 | const args = [chaincodeArg1, "Willy Wonka"]; 66 | 67 | // When 68 | const response = await post( 69 | `/invoke/${channelName}/${chaincodeName}`, 70 | { method, args }, 71 | authorizationHeader(userEnrollToken), 72 | ); 73 | 74 | // Then 75 | expect(response).toEqual( 76 | expect.objectContaining({ 77 | status: 200, 78 | body: { response: { success: "OK" } }, 79 | }), 80 | ); 81 | }); 82 | 83 | it("should allow to query chaincode", async () => { 84 | // Given 85 | const channelName = "my-channel1"; 86 | const chaincodeName = "chaincode1"; 87 | const method = "KVContract:get"; 88 | const args = [chaincodeArg1]; 89 | 90 | // When 91 | const response = await post( 92 | `/query/${channelName}/${chaincodeName}`, 93 | { method, args }, 94 | authorizationHeader(userEnrollToken), 95 | ); 96 | 97 | // Then 98 | expect(response).toEqual( 99 | expect.objectContaining({ 100 | status: 200, 101 | body: { response: { success: "Willy Wonka" } }, 102 | }), 103 | ); 104 | }); 105 | }); 106 | -------------------------------------------------------------------------------- /e2e/testUtils.ts: -------------------------------------------------------------------------------- 1 | import fetch from "node-fetch"; 2 | import * as uuid from "uuid"; 3 | 4 | const port = process.env.PORT ?? "8000"; 5 | const serverPath = `http://localhost:${port}`; 6 | 7 | export const post = ( 8 | path: string, 9 | body: Record, 10 | headers: Record = {}, 11 | ): Promise<{ body: any; status: number }> => 12 | fetch(`${serverPath}${path}`, { 13 | method: "post", 14 | headers: { "content-type": "application/json", ...headers }, 15 | body: JSON.stringify(body), 16 | }).then(async (resp) => ({ 17 | status: resp.status, 18 | body: await resp.json().catch(async (e) => { 19 | console.error(e); 20 | console.log(resp); 21 | return {}; 22 | }), 23 | })); 24 | 25 | export const get = (path: string, headers: Record = {}): Promise<{ body: any; status: number }> => 26 | fetch(`${serverPath}${path}`, { 27 | method: "get", 28 | headers: { "content-type": "application/json", ...headers }, 29 | }).then(async (resp) => ({ 30 | status: resp.status, 31 | body: await resp.json().catch(async (e) => { 32 | console.error(e); 33 | console.log(resp); 34 | return {}; 35 | }), 36 | })); 37 | 38 | const adminCredentials = { id: "admin", secret: "adminpw" }; 39 | 40 | const enroll = async (credentials: { id: string; secret: string }): Promise<{ token: string }> => { 41 | const response = await post("/user/enroll", credentials); 42 | 43 | expect(response).toEqual( 44 | expect.objectContaining({ 45 | status: 200, 46 | body: { token: expect.stringMatching(/.*/) }, 47 | }), 48 | ); 49 | 50 | const token = response.body.token as string; 51 | return { token }; 52 | }; 53 | 54 | export const authorizationHeader = (token: string): { Authorization: string } => ({ Authorization: `Bearer ${token}` }); 55 | 56 | export const enrollAdmin = async (): Promise<{ token: string }> => enroll(adminCredentials); 57 | 58 | export const generateRegisteredUser = async (): Promise<{ id: string; secret: string }> => { 59 | const credentials = { id: `user-${uuid.v1()}`, secret: `secret-${uuid.v1()}` }; 60 | const enrolledAdmin = await enrollAdmin(); 61 | const response = await post("/user/register", credentials, authorizationHeader(enrolledAdmin.token)); 62 | 63 | expect(response).toEqual( 64 | expect.objectContaining({ 65 | status: 201, 66 | body: { message: "ok" }, 67 | }), 68 | ); 69 | 70 | return credentials; 71 | }; 72 | 73 | export const generateEnrolledUser = async (): Promise<{ id: string; token: string }> => { 74 | const credentials = await generateRegisteredUser(); 75 | const { token } = await enroll(credentials); 76 | return { id: credentials.id, token }; 77 | }; 78 | 79 | export const callDefaultChaincode = async ( 80 | token: string, 81 | type: "invoke" | "query", 82 | method: string, 83 | args: string[], 84 | transient?: Record, 85 | ): Promise<{ body: any; status: number }> => { 86 | const req = uuid.v1(); 87 | console.log("--- start", req, type, method); 88 | 89 | const channelName = "my-channel1"; 90 | const chaincodeName = "chaincode1"; 91 | const response = await post( 92 | `/${type}/${channelName}/${chaincodeName}`, 93 | { method, args, transient }, 94 | authorizationHeader(token), 95 | ); 96 | 97 | console.log("--- end", req, type, method); 98 | return response; 99 | }; 100 | -------------------------------------------------------------------------------- /test-network/chaincode-kv-node/index.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const { JSONSerializer } = require("fabric-contract-api"); 3 | const { ChaincodeMockStub } = require("@theledger/fabric-mock-stub"); 4 | const ChaincodeFromContract = require("fabric-shim/lib/contract-spi/chaincodefromcontract"); 5 | const uuid = require("uuid"); 6 | const { contracts } = require("./index"); 7 | 8 | const [KVContract] = contracts; 9 | 10 | const getChaincodeForContract = (contract) => { 11 | const serializers = { 12 | serializers: { 13 | jsonSerializer: JSONSerializer, 14 | }, 15 | transaction: "jsonSerializer", 16 | }; 17 | return new ChaincodeFromContract([contract], serializers, { info: {} }); 18 | }; 19 | 20 | const creatorMock = { 21 | mspid: "MockTestOrgMSP", 22 | idBytes: Buffer.from(`-----BEGIN CERTIFICATE----- 23 | MIIExDCCA6ygAwIBAgIJAK0JmDc/YXWsMA0GCSqGSIb3DQEBBQUAMIGcMQswCQYD 24 | VQQGEwJJTjELMAkGA1UECBMCQVAxDDAKBgNVBAcTA0hZRDEZMBcGA1UEChMQUm9j 25 | a3dlbGwgY29sbGluczEcMBoGA1UECxMTSW5kaWEgRGVzaWduIENlbnRlcjEOMAwG 26 | A1UEAxMFSU1BQ1MxKTAnBgkqhkiG9w0BCQEWGmJyYWphbkBSb2Nrd2VsbGNvbGxp 27 | bnMuY29tMB4XDTExMDYxNjE0MTQyM1oXDTEyMDYxNTE0MTQyM1owgZwxCzAJBgNV 28 | BAYTAklOMQswCQYDVQQIEwJBUDEMMAoGA1UEBxMDSFlEMRkwFwYDVQQKExBSb2Nr 29 | d2VsbCBjb2xsaW5zMRwwGgYDVQQLExNJbmRpYSBEZXNpZ24gQ2VudGVyMQ4wDAYD 30 | VQQDEwVJTUFDUzEpMCcGCSqGSIb3DQEJARYaYnJhamFuQFJvY2t3ZWxsY29sbGlu 31 | cy5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDfjHgUAsbXQFkF 32 | hqv8OTHSzuj+8SKGh49wth3UcH9Nk/YOug7ZvI+tnOcrCZdeG2Ot8Y19Wusf59Y7 33 | q61jSbDWt+7u7P0ylWWcQfCE9IHSiJIaKAklMu2qGB8bFSPqDyVJuWSwcSXEb9C2 34 | xJsabfgJr6mpfWjCOKd58wFprf0RF58pWHyBqBOiZ2U20PKhq8gPJo/pEpcnXTY0 35 | x8bw8LZ3SrrIQZ5WntFKdB7McFKG9yFfEhUamTKOffQ2Y+SDEGVDj3eshF6+Fxgj 36 | 8plyg3tZPRLSHh5DR42HTc/35LA52BvjRMWYzrs4nf67gf652pgHh0tFMNMTMgZD 37 | rpTkyts9AgMBAAGjggEFMIIBATAdBgNVHQ4EFgQUG0cLBjouoJPM8dQzKUQCZYNY 38 | y8AwgdEGA1UdIwSByTCBxoAUG0cLBjouoJPM8dQzKUQCZYNYy8ChgaKkgZ8wgZwx 39 | CzAJBgNVBAYTAklOMQswCQYDVQQIEwJBUDEMMAoGA1UEBxMDSFlEMRkwFwYDVQQK 40 | ExBSb2Nrd2VsbCBjb2xsaW5zMRwwGgYDVQQLExNJbmRpYSBEZXNpZ24gQ2VudGVy 41 | MQ4wDAYDVQQDEwVJTUFDUzEpMCcGCSqGSIb3DQEJARYaYnJhamFuQFJvY2t3ZWxs 42 | Y29sbGlucy5jb22CCQCtCZg3P2F1rDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEB 43 | BQUAA4IBAQCyYZxEzn7203no9TdhtKDWOFRwzYvY2kZppQ/EpzF+pzh8LdBOebr+ 44 | DLRXNh2NIFaEVV0brpQTI4eh6b5j7QyF2UmA6+44zmku9LzS9DQVKGLhIleB436K 45 | ARoWRqxlEK7TF3TauQfaalGH88ZWoDjqqEP/5oWeQ6pr/RChkCHkBSgq6FfGGSLd 46 | ktgFcF0S9U7Ybii/MD+tWMImK8EE3GGgs876yqX/DDhyfW8DfnNZyl35VF/80j/s 47 | 0Lj3F7Po1zsaRbQlhOK5rzRVQA2qnsa4IcQBuYqBWiB6XojPgu9PpRSL7ure7sj6 48 | gRQT0OIU5vXzsmhjqKoZ+dBlh1FpSOX2 49 | -----END CERTIFICATE-----`), 50 | }; 51 | 52 | class KVContractMockStub extends ChaincodeMockStub { 53 | constructor() { 54 | super(KVContract.toString(), getChaincodeForContract(KVContract)); 55 | } 56 | 57 | getBufferArgs() { 58 | return this.getArgs().map((x) => Buffer.from(x)); 59 | } 60 | 61 | getCreator() { 62 | return creatorMock; 63 | } 64 | 65 | async mockInvokeJson(args, transient) { 66 | const transientMap = Object.keys(transient || {}).reduce((map, k) => map.set(k, transient[k]), new Map()); 67 | const response = await this.mockInvoke(uuid.v1(), args, transientMap); 68 | 69 | if (response.status !== 200) { 70 | console.error(`Error invoking chaincode. Status: ${response.status}, message:`, response.message); 71 | throw response.message; 72 | } 73 | 74 | return JSON.parse(response.payload.toString()); 75 | } 76 | } 77 | 78 | describe("KVContract", () => { 79 | it("should put and get value", async () => { 80 | // Given 81 | const stub = new KVContractMockStub(); 82 | const key = "ship"; 83 | const value = "Black Pearl"; 84 | 85 | // When 86 | const putResponse = await stub.mockInvokeJson(["put", key, value]); 87 | const getResponse = await stub.mockInvokeJson(["get", key]); 88 | 89 | // Then 90 | expect(putResponse).toEqual({ success: "OK" }); 91 | expect(getResponse).toEqual({ success: "Black Pearl" }); 92 | }); 93 | 94 | it("should return missing value error", async () => { 95 | // Given 96 | const stub = new KVContractMockStub(); 97 | const key = "ship"; 98 | 99 | // When 100 | const getResponse = await stub.mockInvokeJson(["get", key]); 101 | 102 | // Then 103 | expect(getResponse).toEqual({ error: "NOT_FOUND" }); 104 | }); 105 | 106 | it("should put private data", async () => { 107 | // Given 108 | const stub = new KVContractMockStub(); 109 | const key = "diary"; 110 | const message = "Black Pearl was initially owned by Jack Sparrow"; 111 | 112 | // When 113 | const response = await stub.mockInvokeJson(["putPrivateMessage", key], { message }); 114 | 115 | // Then 116 | expect(response).toEqual({ success: "OK" }); 117 | }); 118 | 119 | // Just for the reference. Method 'getPrivateDataHash' is not implemented in @theledger/fabric-mock-stub lib 120 | it.skip("should verify private data", async () => { 121 | // Given 122 | const stub = new KVContractMockStub(); 123 | const key = "diary"; 124 | const message = { message: "Black Pearl was initially owned by Jack Sparrow" }; 125 | const wrongMessage = { message: "Owned by Willy Wonka" }; 126 | await stub.mockInvokeJson(["putPrivateMessage", key], message); 127 | 128 | // When 129 | const validResponse = await stub.mockInvokeJson(["verifyPrivateMessage", key], message); 130 | const invalidResponse = await stub.mockInvokeJson(["verifyPrivateMessage", key], wrongMessage); 131 | 132 | // Then 133 | expect(validResponse).toEqual({ success: "OK" }); 134 | expect(invalidResponse).toEqual({ success: "OK" }); 135 | }); 136 | }); 137 | -------------------------------------------------------------------------------- /src/NetworkPool.ts: -------------------------------------------------------------------------------- 1 | import NodeCache from "node-cache"; 2 | import * as grpc from "@grpc/grpc-js"; 3 | import { connect, Gateway, Identity, Signer, signers } from "@hyperledger/fabric-gateway"; 4 | import crypto from "crypto"; 5 | import { Client, User } from "fabric-common"; 6 | import { CachedIdentity } from "./IdentityCache"; 7 | import config from "./config"; 8 | import DiscoveryService from "./DiscoveryService"; 9 | 10 | const networksCache = new NodeCache({ stdTTL: 60 * 5, useClones: false }); 11 | networksCache.on("del", (_, value) => { 12 | (value as Gateway)?.close(); 13 | }); 14 | 15 | const logger = config.getLogger("FabloRestNetworkPool"); 16 | 17 | interface CachedNetwork { 18 | gateway: Gateway; 19 | channelName: string; 20 | } 21 | 22 | const createGatewayWithEndpoint = (identity: CachedIdentity, dc: any): Gateway => { 23 | const address = dc.url.replace(/^grpcs?:\/\//, ""); 24 | const credentials = dc.pem ? grpc.credentials.createSsl(Buffer.from(dc.pem)) : grpc.credentials.createInsecure(); 25 | 26 | const options: grpc.ClientOptions = {}; 27 | if (dc["ssl-target-name-override"]) { 28 | options["grpc.ssl_target_name_override"] = dc["ssl-target-name-override"]; 29 | } 30 | 31 | const client = new grpc.Client(address, credentials, options); 32 | 33 | // Extract from the stored identity object 34 | const signer: Signer = signers.newPrivateKeySigner(crypto.createPrivateKey(identity.identity.credentials.privateKey)); 35 | 36 | const gwIdentity: Identity = { 37 | mspId: identity.identity.mspId, 38 | credentials: Buffer.from(identity.identity.credentials.certificate), 39 | }; 40 | 41 | return connect({ 42 | client, 43 | identity: gwIdentity, 44 | signer, 45 | evaluateOptions: () => ({ deadline: Date.now() + 5000 }), 46 | endorseOptions: () => ({ deadline: Date.now() + 15000 }), 47 | submitOptions: () => ({ deadline: Date.now() + 5000 }), 48 | commitStatusOptions: () => ({ deadline: Date.now() + 60000 }), 49 | }); 50 | }; 51 | 52 | const createGateway = (identity: CachedIdentity, channelName: string): Gateway => { 53 | const configs = config.discovererConfigs; 54 | if (configs.length === 0) throw new Error("No discovery endpoints configured"); 55 | 56 | let lastError: Error | undefined; 57 | 58 | for (let i = 0; i < configs.length; i++) { 59 | try { 60 | const gateway = createGatewayWithEndpoint(identity, configs[i]); 61 | 62 | // Test the connection by attempting to get the network 63 | // This will fail if the peer doesn't have access to the channel 64 | gateway.getNetwork(channelName); 65 | 66 | logger.debug(`Connected to ${configs[i].url} for channel=${channelName}, user=${identity.user.getName()}`); 67 | return gateway; 68 | } catch (e: any) { 69 | lastError = e; 70 | logger.debug(`Failed to connect to ${configs[i].url} for channel=${channelName}: ${e.message}`); 71 | } 72 | } 73 | 74 | throw new Error(`No available gateways for channel ${channelName}. Last error: ${lastError?.message}`); 75 | }; 76 | 77 | const getNetwork = async (identity: CachedIdentity, channelName: string): Promise => { 78 | const key = `${identity.user.getName()}-${channelName}`; 79 | const cached = networksCache.get(key); 80 | 81 | if (cached) { 82 | logger.debug(`Got network from cache (user=${identity.user.getName()}, channel=${channelName})`); 83 | return cached; 84 | } 85 | 86 | logger.debug(`Creating new network (user=${identity.user.getName()}, channel=${channelName})`); 87 | const gateway = createGateway(identity, channelName); 88 | const network = { gateway, channelName }; 89 | networksCache.set(key, network); 90 | return network; 91 | }; 92 | 93 | const discover = async (user: User, channelName: string): Promise> => { 94 | const userId = user.getName(); 95 | const client = Client.newClient(`client-${userId}`); 96 | const discovery = await DiscoveryService.create(client, channelName, user); 97 | return discovery.getDiscoveryResults(true); 98 | }; 99 | 100 | const invoke = async ( 101 | identity: CachedIdentity, 102 | channelName: string, 103 | chaincodeName: string, 104 | method: string, 105 | args: string[], 106 | transient?: Record, 107 | ): Promise => { 108 | const { gateway } = await getNetwork(identity, channelName); 109 | const network = gateway.getNetwork(channelName); 110 | const contract = network.getContract(chaincodeName); 111 | 112 | const proposal = contract.newProposal(method, { 113 | arguments: args, 114 | transientData: transient, 115 | }); 116 | 117 | const transaction = await proposal.endorse(); 118 | const commit = await transaction.submit(); 119 | const status = await commit.getStatus(); 120 | 121 | if (status.code !== 0) { 122 | const error = new Error(`Transaction failed with code ${status.code}`); 123 | (error as any).transactionCode = status.code.toString(); 124 | throw error; 125 | } 126 | 127 | return Buffer.from(transaction.getResult()); 128 | }; 129 | 130 | const query = async ( 131 | identity: CachedIdentity, 132 | channelName: string, 133 | chaincodeName: string, 134 | method: string, 135 | args: string[], 136 | transient?: Record, 137 | ): Promise => { 138 | const { gateway } = await getNetwork(identity, channelName); 139 | const network = gateway.getNetwork(channelName); 140 | const contract = network.getContract(chaincodeName); 141 | 142 | const proposal = contract.newProposal(method, { 143 | arguments: args, 144 | transientData: transient, 145 | }); 146 | 147 | const result = await proposal.evaluate(); 148 | return Buffer.from(result); 149 | }; 150 | 151 | export default { discover, invoke, query }; 152 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import cors from "cors"; 3 | import bearerToken from "express-bearer-token"; 4 | import FabricCAServices from "fabric-ca-client"; 5 | import NetworkPool from "./NetworkPool"; 6 | import IdentityCache from "./IdentityCache"; 7 | import config from "./config"; 8 | import Authorization from "./Authorization"; 9 | import ChaincodeRequest from "./ChaincodeRequest"; 10 | import { Utils } from "fabric-common"; 11 | import TransactionResult from "./TransactionResult"; 12 | 13 | const logger = Utils.getLogger("FabloRest"); 14 | 15 | const app = express(); 16 | app.use(express.json({ type: () => "json" })); 17 | app.use(cors()); 18 | app.use(bearerToken()); 19 | 20 | app.use((req, res, next) => { 21 | res.set("Cache-Control", "no-store"); 22 | logger.debug(`${req.method} ${req.path}`); 23 | next(); 24 | }); 25 | 26 | const ca = new FabricCAServices(config.FABRIC_CA_URL, undefined, config.FABRIC_CA_NAME); 27 | 28 | app.post("/user/enroll", async (req, res) => { 29 | const id: string = req.body.id; 30 | const secret: string = req.body.secret; 31 | logger.debug("Enrolling as user %s", id); 32 | 33 | try { 34 | const enrollResp = await ca.enroll({ enrollmentID: id, enrollmentSecret: secret }); 35 | const token = await IdentityCache.put(id, enrollResp.key, enrollResp.certificate, config.MSP_ID); 36 | res.status(200).send({ token }); 37 | } catch (e: any) { 38 | res.status(400).send({ message: e.message }); 39 | } 40 | }); 41 | 42 | app.post("/user/reenroll", async (req, res) => { 43 | const caller = await Authorization.getFromToken(req, res); 44 | if (!caller) { 45 | return; 46 | } 47 | 48 | const id = caller.user.getName(); 49 | logger.debug("Re enrolling user %s", id); 50 | 51 | try { 52 | const enrollResp = await ca.reenroll(caller.user, []); 53 | const token = await IdentityCache.put(id, enrollResp.key, enrollResp.certificate, config.MSP_ID); 54 | IdentityCache.del(caller.token); 55 | res.status(200).send({ token }); 56 | } catch (e: any) { 57 | res.status(400).send({ message: e.message }); 58 | } 59 | }); 60 | 61 | app.post("/user/register", async (req, res) => { 62 | const caller = await Authorization.getFromToken(req, res); 63 | if (!caller) { 64 | return; 65 | } 66 | 67 | const id = req.body.id; 68 | const secret = req.body.secret; 69 | logger.debug("Registering user %s by %s", id, caller.user.getName()); 70 | 71 | const registerRequest = { 72 | enrollmentID: id, 73 | enrollmentSecret: secret, 74 | affiliation: "", // not supported yet 75 | maxEnrollments: 0, 76 | }; 77 | 78 | try { 79 | await ca.register(registerRequest, caller.user); 80 | return res.status(201).send({ message: "ok" }); 81 | } catch (e: any) { 82 | return res.status(400).send({ message: e.message }); 83 | } 84 | }); 85 | 86 | app.get("/user/identities", async (req, res) => { 87 | const caller = await Authorization.getFromToken(req, res); 88 | if (!caller) { 89 | return; 90 | } 91 | 92 | logger.debug("Retrieving user list for user %s", caller.user.getName()); 93 | 94 | try { 95 | const response = await ca.newIdentityService().getAll(caller.user); 96 | if (response.result) { 97 | return res.status(200).send({ response: response.result }); 98 | } else { 99 | return res.status(400).send({ ...response, message: "Cannot get identities" }); 100 | } 101 | } catch (e: any) { 102 | return res.status(400).send({ message: e.message }); 103 | } 104 | }); 105 | 106 | app.post("/discover/:channelName", async (req, res) => { 107 | const identity = await Authorization.getFromToken(req, res); 108 | 109 | if (!identity) { 110 | return; 111 | } 112 | 113 | try { 114 | const response = await NetworkPool.discover(identity.user, req.params.channelName); 115 | res.status(200).send({ response }); 116 | } catch (e: any) { 117 | res.status(500).send({ message: e.message }); 118 | } 119 | }); 120 | 121 | app.post("/invoke/:channelName/:chaincodeName", async (req, res) => { 122 | const identity = await Authorization.getFromToken(req, res); 123 | const chaincodeReq = ChaincodeRequest.getValid(req, res); 124 | 125 | if (!identity || !chaincodeReq) { 126 | return; 127 | } 128 | 129 | logger.debug("Invoking chaincode %s by user %s", chaincodeReq.method, identity.user.getName()); 130 | 131 | try { 132 | const transactionResult = await NetworkPool.invoke( 133 | identity, 134 | chaincodeReq.channelName, 135 | chaincodeReq.chaincodeName, 136 | chaincodeReq.method, 137 | chaincodeReq.args, 138 | chaincodeReq.transient, 139 | ); 140 | 141 | const { status, response } = TransactionResult.parse(transactionResult); 142 | res.status(status).send({ response }); 143 | } catch (e: any) { 144 | res.status(400).send({ message: e.transactionCode ?? e.message }); 145 | } 146 | }); 147 | 148 | app.post("/query/:channelName/:chaincodeName", async (req, res) => { 149 | const identity = await Authorization.getFromToken(req, res); 150 | const chaincodeReq = ChaincodeRequest.getValid(req, res); 151 | 152 | if (!identity || !chaincodeReq) { 153 | return; 154 | } 155 | 156 | logger.debug("Querying chaincode %s by user %s", chaincodeReq.method, identity.user.getName()); 157 | 158 | try { 159 | const transactionResult = await NetworkPool.query( 160 | identity, 161 | chaincodeReq.channelName, 162 | chaincodeReq.chaincodeName, 163 | chaincodeReq.method, 164 | chaincodeReq.args, 165 | chaincodeReq.transient, 166 | ); 167 | 168 | const { status, response } = TransactionResult.parse(transactionResult); 169 | res.status(status).send({ response }); 170 | } catch (e: any) { 171 | res.status(400).send({ message: e.transactionCode ?? e.message }); 172 | } 173 | }); 174 | 175 | app.listen(config.PORT, () => { 176 | logger.info(`⚡️[server]: Server is running at https://localhost:${config.PORT} for organization ${config.MSP_ID}`); 177 | }); 178 | -------------------------------------------------------------------------------- /e2e/authorization.test.ts: -------------------------------------------------------------------------------- 1 | import * as uuid from "uuid"; 2 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 3 | // @ts-ignore 4 | import { post, generateRegisteredUser, generateEnrolledUser, enrollAdmin, get, authorizationHeader } from "./testUtils"; 5 | import config from "../src/config"; 6 | 7 | jest.setTimeout(60000); 8 | 9 | describe("Enrollment", () => { 10 | it("fail to enroll admin in case of invalid credentials", async () => { 11 | // When 12 | const response = await post("/user/enroll", { id: "not-present", secret: "invalid" }); 13 | 14 | // Then 15 | expect(response).toEqual( 16 | expect.objectContaining({ 17 | status: 400, 18 | body: { 19 | // default message from Hyperledger Fabric CA 20 | message: "fabric-ca request enroll failed with errors [[ { code: 20, message: 'Authentication failure' } ]]", 21 | }, 22 | }), 23 | ); 24 | }); 25 | 26 | it("should allow to enroll concurrently (and allow to enroll twice)", async () => { 27 | // Given 28 | const userCredentials = await generateRegisteredUser(); 29 | 30 | // When 31 | const [response1, response2] = await Promise.all([ 32 | post("/user/enroll", userCredentials), 33 | post("/user/enroll", userCredentials), 34 | ]); 35 | 36 | // Then 37 | const responseMatcher = expect.objectContaining({ 38 | status: 200, 39 | body: { token: expect.stringMatching(/.*/) }, 40 | }); 41 | expect(response1).toEqual(responseMatcher); 42 | expect(response2).not.toEqual(response1); 43 | expect(response2).toEqual(responseMatcher); 44 | }); 45 | 46 | it("should allow to reenroll", async () => { 47 | // Given 48 | const { token } = await generateEnrolledUser(); 49 | 50 | // When 51 | const response = await post("/user/reenroll", {}, authorizationHeader(token)); 52 | 53 | // Then 54 | expect(response).toEqual( 55 | expect.objectContaining({ 56 | status: 200, 57 | body: { token: expect.stringMatching(/.*/) }, 58 | }), 59 | ); 60 | }); 61 | 62 | it("should allow to perform user action by reenrolled user", async () => { 63 | // given 64 | const { token } = await enrollAdmin(); 65 | const reenrollResponse = await post("/user/reenroll", {}, authorizationHeader(token)); 66 | 67 | // when 68 | const response = await post( 69 | "/user/register", 70 | { id: uuid.v1(), secret: "aaabbbccc" }, 71 | authorizationHeader(reenrollResponse.body.token), 72 | ); 73 | 74 | // Then 75 | expect(response).toEqual( 76 | expect.objectContaining({ 77 | status: 201, 78 | body: { message: "ok" }, 79 | }), 80 | ); 81 | }); 82 | 83 | it("should not allow to perform action with old token invalidated by reenrollment", async () => { 84 | // given 85 | const { token } = await enrollAdmin(); 86 | await post("/user/reenroll", {}, authorizationHeader(token)); 87 | 88 | // when 89 | const response = await post("/user/register", { id: uuid.v1(), secret: "aaabbbccc" }, authorizationHeader(token)); 90 | 91 | // Then 92 | expect(response).toEqual( 93 | expect.objectContaining({ 94 | status: 403, 95 | body: { message: "User with provided token is not enrolled" }, 96 | }), 97 | ); 98 | }); 99 | }); 100 | 101 | describe("Registration", () => { 102 | it("should fail to register user in case of missing Authorization header", async () => { 103 | // When 104 | const response = await post("/user/register", { id: uuid.v1(), secret: "aaabbbccc" }); 105 | 106 | // Then 107 | expect(response).toEqual( 108 | expect.objectContaining({ 109 | status: 400, 110 | body: { message: "Missing authorization header" }, 111 | }), 112 | ); 113 | }); 114 | 115 | it("should fail to register user in case of invalid Authorization header", async () => { 116 | // When 117 | const response = await post( 118 | "/user/register", 119 | { id: uuid.v1(), secret: "aaabbbccc" }, 120 | authorizationHeader("invalid-token"), 121 | ); 122 | 123 | // Then 124 | expect(response).toEqual( 125 | expect.objectContaining({ 126 | status: 403, 127 | body: { message: "User with provided token is not enrolled" }, 128 | }), 129 | ); 130 | }); 131 | 132 | it("should not allow to register user by non-admin", async () => { 133 | // Given 134 | const { token } = await generateEnrolledUser(); 135 | 136 | // When 137 | const response = await post("/user/register", { id: uuid.v1(), secret: "aaabbbccc" }, authorizationHeader(token)); 138 | 139 | // Then 140 | expect(response).toEqual( 141 | expect.objectContaining({ 142 | status: 400, 143 | body: { 144 | message: expect.stringContaining( 145 | "fabric-ca request register failed with errors [[ { code: 71, message: 'Authorization failure' } ]]", 146 | ), 147 | }, 148 | }), 149 | ); 150 | }); 151 | }); 152 | 153 | describe("Identities", () => { 154 | it("should list identities for an admin", async () => { 155 | // Given 156 | const { token } = await enrollAdmin(); 157 | const user = await generateRegisteredUser(); 158 | 159 | // When 160 | const response = await get("/user/identities", authorizationHeader(token)); 161 | 162 | // Then 163 | expect(response).toEqual( 164 | expect.objectContaining({ 165 | status: 200, 166 | body: { 167 | response: { 168 | caname: config.FABRIC_CA_NAME, 169 | identities: expect.anything(), 170 | }, 171 | }, 172 | }), 173 | ); 174 | 175 | expect(response.body.response.identities).toEqual( 176 | expect.arrayContaining([ 177 | expect.objectContaining({ id: "admin", type: "client" }), 178 | expect.objectContaining({ id: user.id, type: "client" }), 179 | ]), 180 | ); 181 | }); 182 | 183 | it("should fail to list identities for a non-admin user", async () => { 184 | // Given 185 | const { token } = await generateEnrolledUser(); 186 | 187 | // When 188 | const response = await get("/user/identities", authorizationHeader(token)); 189 | 190 | // Then 191 | expect(response).toEqual( 192 | expect.objectContaining({ 193 | status: 400, 194 | body: { message: expect.stringContaining("Authorization failure") }, 195 | }), 196 | ); 197 | }); 198 | }); 199 | -------------------------------------------------------------------------------- /e2e/discover.test.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 2 | // @ts-ignore 3 | import { authorizationHeader, generateEnrolledUser, post } from "./testUtils"; 4 | import config from "../src/config"; 5 | 6 | jest.setTimeout(10000); 7 | 8 | describe("Discover scenario", () => { 9 | const credentials = generateEnrolledUser(); 10 | 11 | it("should get discovery results for channel", async () => { 12 | // Given 13 | const { token } = await credentials; 14 | const channelName = "my-channel1"; 15 | 16 | // When 17 | const response = await post(`/discover/${channelName}`, {}, authorizationHeader(token)); 18 | 19 | // Then 20 | expect(response).toEqual( 21 | expect.objectContaining({ 22 | status: 200, 23 | body: { 24 | response: { 25 | msps: { 26 | OrdererMSP: { 27 | admins: expect.stringContaining("-----BEGIN CERTIFICATE-----"), 28 | id: "OrdererMSP", 29 | intermediateCerts: "", 30 | name: "OrdererMSP", 31 | organizationalUnitIdentifiers: [], 32 | rootCerts: expect.stringContaining("-----BEGIN CERTIFICATE-----"), 33 | tlsIntermediateCerts: "", 34 | tlsRootCerts: expect.stringContaining("-----BEGIN CERTIFICATE-----"), 35 | }, 36 | Org1MSP: { 37 | admins: expect.stringContaining("-----BEGIN CERTIFICATE-----"), 38 | id: "Org1MSP", 39 | intermediateCerts: "", 40 | name: "Org1MSP", 41 | organizationalUnitIdentifiers: [], 42 | rootCerts: expect.stringContaining("-----BEGIN CERTIFICATE-----"), 43 | tlsIntermediateCerts: "", 44 | tlsRootCerts: expect.stringContaining("-----BEGIN CERTIFICATE-----"), 45 | }, 46 | Org2MSP: { 47 | admins: expect.stringContaining("-----BEGIN CERTIFICATE-----"), 48 | id: "Org2MSP", 49 | intermediateCerts: "", 50 | name: "Org2MSP", 51 | organizationalUnitIdentifiers: [], 52 | rootCerts: expect.stringContaining("-----BEGIN CERTIFICATE-----"), 53 | tlsIntermediateCerts: "", 54 | tlsRootCerts: expect.stringContaining("-----BEGIN CERTIFICATE-----"), 55 | }, 56 | }, 57 | orderers: { 58 | OrdererMSP: { 59 | endpoints: [ 60 | { 61 | host: "orderer0.group1.root.com", 62 | name: "orderer0.group1.root.com:7030", 63 | port: 7030, 64 | }, 65 | ], 66 | }, 67 | }, 68 | peers_by_org: { 69 | Org1MSP: { 70 | peers: expect.arrayContaining([ 71 | { 72 | chaincodes: [ 73 | { name: "chaincode1", version: "1" }, 74 | { name: "_lifecycle", version: "1" }, 75 | ], 76 | endpoint: "peer0.org1.com:7041", 77 | ledgerHeight: expect.objectContaining({ 78 | high: expect.any(Number), 79 | low: expect.any(Number), 80 | }), 81 | mspid: "Org1MSP", 82 | name: "peer0.org1.com:7041", 83 | }, 84 | { 85 | chaincodes: [ 86 | { name: "chaincode1", version: "1" }, 87 | { name: "_lifecycle", version: "1" }, 88 | ], 89 | endpoint: "peer1.org1.com:7042", 90 | ledgerHeight: expect.objectContaining({ 91 | high: expect.any(Number), 92 | low: expect.any(Number), 93 | }), 94 | mspid: "Org1MSP", 95 | name: "peer1.org1.com:7042", 96 | }, 97 | ]), 98 | }, 99 | Org2MSP: { 100 | peers: expect.arrayContaining([ 101 | { 102 | chaincodes: [ 103 | { name: "chaincode1", version: "1" }, 104 | { name: "_lifecycle", version: "1" }, 105 | ], 106 | endpoint: "peer0.org2.com:7061", 107 | ledgerHeight: expect.objectContaining({ 108 | high: expect.any(Number), 109 | low: expect.any(Number), 110 | }), 111 | mspid: "Org2MSP", 112 | name: "peer0.org2.com:7061", 113 | }, 114 | { 115 | chaincodes: [ 116 | { name: "chaincode1", version: "1" }, 117 | { name: "_lifecycle", version: "1" }, 118 | ], 119 | endpoint: "peer1.org2.com:7062", 120 | ledgerHeight: expect.objectContaining({ 121 | high: expect.any(Number), 122 | low: expect.any(Number), 123 | }), 124 | mspid: "Org2MSP", 125 | name: "peer1.org2.com:7062", 126 | }, 127 | ]), 128 | }, 129 | }, 130 | timestamp: expect.anything(), 131 | }, 132 | }, 133 | }), 134 | ); 135 | }); 136 | 137 | it("should get discovery results for both channels", async () => { 138 | /* Rationale: Fabric's DiscoveryService uses first endpoint and fails if, for instance, 139 | * given peer did not join the channel. That's why we use a kind of round robin strategy 140 | * for service discovery. If one of the peers fail, we did not return error, but query 141 | * another one. And this is the thing the test aims to verify. 142 | */ 143 | // Given 144 | const discoveryEndpoints = config.discovererConfigs.map((d) => d.url); 145 | expect(discoveryEndpoints).toEqual([ 146 | "grpcs://peer0.org2.com:7061", // has access to my-channel1 147 | "grpcs://wrong.org2.com:9999", // unavailable 148 | "grpcs://peer1.org2.com:7062", // has access to my-channel1 and my-channel2 149 | ]); 150 | 151 | const { token } = await credentials; 152 | const easyChannel = "my-channel1"; // first discoverer will return the results 153 | const hardChannel = "my-channel2"; // third disciverer will return results 154 | const errorChannel = "my-channel-non-existing"; // no disciverer will return results 155 | 156 | const getEndpoints = (response: Record) => { 157 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 158 | // @ts-ignore 159 | const fromOrg1 = response.body.response.peers_by_org.Org1MSP.peers.map((p) => p.endpoint); 160 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 161 | // @ts-ignore 162 | const fromOrg2 = response.body.response.peers_by_org.Org2MSP.peers.map((p) => p.endpoint); 163 | return fromOrg1.concat(fromOrg2).sort(); 164 | }; 165 | 166 | // When 167 | const easyResponse = await post(`/discover/${easyChannel}`, {}, authorizationHeader(token)); 168 | const hardResponse = await post(`/discover/${hardChannel}`, {}, authorizationHeader(token)); 169 | const errorResponse = await post(`/discover/${errorChannel}`, {}, authorizationHeader(token)); 170 | 171 | // Then 172 | expect(easyResponse).toEqual(expect.objectContaining({ status: 200, body: expect.anything() })); 173 | expect(getEndpoints(easyResponse)).toEqual([ 174 | "peer0.org1.com:7041", 175 | "peer0.org2.com:7061", 176 | "peer1.org1.com:7042", 177 | "peer1.org2.com:7062", 178 | ]); 179 | 180 | expect(hardResponse).toEqual(expect.objectContaining({ status: 200, body: expect.anything() })); 181 | expect(getEndpoints(hardResponse)).toEqual(["peer1.org1.com:7042", "peer1.org2.com:7062"]); 182 | 183 | expect(errorResponse).toEqual({ 184 | status: 500, 185 | body: { 186 | message: `No available discoverers for channel ${errorChannel}`, 187 | }, 188 | }); 189 | }); 190 | }); 191 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fablo REST 2 | 3 | [![Test](https://github.com/fablo-io/fablo-rest/actions/workflows/test.yml/badge.svg)](https://github.com/fablo-io/fablo-rest/actions/workflows/test.yml) 4 | 5 | A simple REST API interface for Hyperledger Fabric blockchain network. Supported features: 6 | 7 | * enroll, reenroll, register and list identities, 8 | * discover the network for a given channel, 9 | * query and invoke chaincode (with transient parameters support). 10 | 11 | Fablo REST should work with any available Hyperledger Fabric network, however it is also integrated 12 | with [Fablo](https://github.com/hyperledger-labs/fablo), a simple tool to generate the Hyperledger Fabric blockchain network 13 | and run it on Docker. It is distributed as the Docker 14 | image: `ghcr.io/fablo-io/fablo-rest`. 15 | 16 | ## Running and configuration 17 | 18 | ### Running example 1 19 | 20 | Use Fablo REST in Docker compose file within the same Docker network as Hyperledger Fabric: 21 | 22 | ```yaml 23 | fablo-rest.org1.com: 24 | image: ghcr.io/fablo-io/fablo-rest:0.1.2 25 | environment: 26 | - PORT=8000 27 | - MSP_ID=Org1MSP 28 | - FABRIC_CA_URL=http://ca.org1.com:7054 29 | - FABRIC_CA_NAME=ca.org1.com 30 | - AS_LOCALHOST=false 31 | - DISCOVERY_URLS=grpc://peer0.org1.com:7060 32 | - HFC_LOGGING={"debug":"console"} 33 | ports: 34 | - 8800:8000 35 | depends_on: 36 | - ca.org1.com 37 | - peer0.org1.com 38 | ``` 39 | 40 | ### Running example 2 41 | 42 | Start Fablo REST locally, connected to some Hyperedger Fabric network that has been started 43 | on `localhost` (+ use TLS): 44 | 45 | ```bash 46 | npm install 47 | npm run build 48 | 49 | MSP_ID="Org2MSP" \ 50 | FABRIC_CA_URL="https://localhost:7054" \ 51 | FABRIC_CA_NAME="ca.org2.com" \ 52 | DISCOVERY_URLS="grpcs://localhost:7070,grpcs://localhost:7071" \ 53 | DISCOVERY_SSL_TARGET_NAME_OVERRIDES="peer0.org2.com,peer1.org2.com" \ 54 | DISCOVERY_TLS_CA_CERT_FILES="./some-path/org2.com/peers/peer0.org2.com/tls/ca.crt,./some-path/org2.com/peers/peer1.org2.com/tls/ca.crt" \ 55 | AS_LOCALHOST="true" \ 56 | npm start-dist 57 | ``` 58 | 59 | ### Running example 3 60 | 61 | Start Fablo REST as a Docker container and join some Docker network: 62 | 63 | ```bash 64 | docker run \ 65 | -e MSP_ID="Org1MSP" \ 66 | -e FABRIC_CA_URL="http://ca.org1.com:7054" \ 67 | -e FABRIC_CA_NAME="ca.org1.com" \ 68 | -e DISCOVERY_URLS="grpc://peer0.org1.com:7060,grpc://peer0.org2.com:7060" \ 69 | -e AS_LOCALHOST="false" \ 70 | -p "8000:8000" \ 71 | --network="$docker_network_name" \ 72 | -d \ 73 | --rm \ 74 | ghcr.io/fablo-io/fablo-rest:0.1.2 75 | ``` 76 | 77 | ### Environment variables 78 | 79 | * `PORT` - the port under with Fablo REST will be available (default: `8000`). 80 | * `MSP_ID` - a Membership Service Provider ID for the organization that runs the Fablo REST instance ( 81 | default: `Org1MSP`). 82 | * `FABRIC_CA_URL` - an URL to Certificate Authority (CA) instance (default: `http://localhost:7031`). 83 | * `FABRIC_CA_NAME` - the name of CA used by this Fablo REST instance (default: `ca.org1.com`). 84 | * `AS_LOCALHOST` - whether the service discovery should convert discovered host names to `localhost`. The variable 85 | should be set to `true` if the network starts with Docker compose (default: `true`). 86 | * `DISCOVERY_URLS` - a comma-separated list of Anchor Peer URLs to be used as service discovery endpoins ( 87 | default: `grpc://localhost:7060`). 88 | * `DISCOVERY_SSL_TARGET_NAME_OVERRIDES` - if `AS_LOCALHOST` variable is set to `true` and the Fabric network uses TLS. 89 | The variable should provide a comma-separated list of anchor peer names for each value from `DISCOVERY_URLS` variable. 90 | See also `[ConnectOptions](https://hyperledger.github.io/fabric-sdk-node/release-2.2/global.html#ConnectOptions)` from 91 | Fabric Node.js SDK. By default the variable is empty. 92 | * `DISCOVERY_TLS_CA_CERT_FILES` - if the Fabric network uses TLS, the variable should provide a comma separated paths to 93 | PEM certificates of Anchor Peers specified in `DISCOVERY_URLS` variable. Paths should be available for Fablo REST 94 | container, so don't forget to mount required volumes. By default the variable is empty. 95 | * `HFC_LOGGING` - contains a stringified JSON that describes the Logging levels and targets for both Fablo REST and 96 | Node.js SDK that Fablo REST uses, 97 | see [some examples](https://hyperledger.github.io/fabric-sdk-node/release-1.4/tutorial-logging.html). 98 | 99 | ## Endpoints 100 | 101 | ### POST /user/enroll 102 | 103 | Enrolls an identity in the CA and returns Fablo REST authorization token. 104 | 105 | #### Request 106 | 107 | ```bash 108 | curl --request POST \ 109 | --url http://localhost:8000/user/enroll \ 110 | --header 'Authorization: Bearer ' \ 111 | --data '{"id": "", "secret": ""}' 112 | ``` 113 | 114 | #### Response body 115 | 116 | ```json 117 | { 118 | "token": "" 119 | } 120 | ``` 121 | 122 | ### POST /user/reenroll 123 | 124 | Reenrolls an identity in the CA, returns new Fablo REST authorization token and invalidates the previous one. 125 | 126 | #### Request 127 | 128 | ```bash 129 | curl --request POST \ 130 | --url http://localhost:8000/user/reenroll \ 131 | --header 'Authorization: Bearer ' 132 | ``` 133 | 134 | #### Response body 135 | 136 | ```json 137 | { 138 | "token": "" 139 | } 140 | ``` 141 | 142 | ### POST /user/register 143 | 144 | Registers an identity in the CA. Requires an admin user. 145 | 146 | #### Request 147 | 148 | ```bash 149 | curl --request POST \ 150 | --url http://localhost:8000/user/register \ 151 | --header 'Authorization: Bearer ' 152 | --data '{"id": "", "secret": ""}' 153 | ``` 154 | 155 | #### Response body 156 | 157 | ```json 158 | { 159 | "message": "ok" 160 | } 161 | ``` 162 | 163 | ### GET /user/identities 164 | 165 | Returns a list of identities saved in the CA. Requires an admin user. 166 | 167 | #### Request 168 | 169 | ```bash 170 | curl --request GET \ 171 | --url http://localhost:8000/user/identities \ 172 | --header 'Authorization: Bearer ' 173 | ``` 174 | 175 | #### Response body 176 | 177 | ```json 178 | { 179 | "response": { 180 | "caname": "", 181 | "identities": [ 182 | { 183 | "affiliation": "", 184 | "id": "", 185 | "type": "", 186 | "attrs": "", 187 | "max_enrollments": "" 188 | }, 189 | ... 190 | ] 191 | } 192 | } 193 | ``` 194 | 195 | ### POST /discover/:channel 196 | 197 | Runs service discovery for given `channel` and returns the discovery results. It uses kind of "round robin" strategy for 198 | the discovery. If a discovery peer is not available for discovery for the given channel, it tries another one. 199 | 200 | #### Request 201 | 202 | ```bash 203 | curl --request POST \ 204 | --url http://localhost:8000/discover/my-channel1 \ 205 | --header 'Authorization: Bearer ' 206 | ``` 207 | 208 | #### Response body 209 | 210 | JSON with discovery results, containing MSPs, orderers and peers by organizations. This is quite a complex object, see 211 | the example in the [discovery e2e tests](https://github.com/fablo-io/fablo-rest/blob/main/e2e/discover.test.ts). 212 | 213 |

POST /invoke/:channel/:chaincode
POST /query/:channel/:chaincode

214 | 215 | Invokes or queries the `chaincode` on a given `channel`. It uses `MSPID_SCOPE_ALLFORTX` strategy for invoke 216 | and `MSPID_SCOPE_ROUND_ROBIN` strategy for query. 217 | 218 | ### Request 219 | 220 | ```bash 221 | curl --request POST \ 222 | --url http://localhost:8000/invoke/my-channel1/chaincode1 \ 223 | --header 'Authorization: Bearer ' 224 | --data "{ 225 | \"method\": \"\", 226 | \"args\": [ 227 | \"arg1\", 228 | \"arg2\", 229 | ... 230 | ], 231 | \"transient\": { 232 | \"\": \"\", 233 | ... 234 | } 235 | }" 236 | ``` 237 | 238 | You may use `invoke` and `query` interchangeably (the rest of the request is the same), but remember that `query` 239 | requests do not modify the blockchain. 240 | 241 | Field `transient` is optional, and it contains an object with string keys and string values. You don't need to encode 242 | the stings in Base64 format, Fablo REST will do it for you. 243 | 244 | #### Response body 245 | 246 | A response from the chaincode. It will contain a JSON object, string or another value returned by the chaincode: 247 | 248 | ```json 249 | { 250 | "response": 251 | } 252 | ``` 253 | 254 | Besides, you may implement your chaincode to return the following object: 255 | 256 | ```json 257 | { 258 | "status": , 259 | "response": 260 | } 261 | ``` 262 | 263 | In this case Fablo REST will use the `status` as HTTP status code and a `response` as a `response` inside the response 264 | body. 265 | 266 | ### Error responses 267 | 268 | In case of errors along with appropriate HTTP response code, Fablo REST returns the following 269 | body: `{ "message": "" }`. The error message may be provided by Fablo REST or just passed from Fabric CA 270 | or other nodes in the network. 271 | 272 | ### Sample usage 273 | 274 | See more usage examples in our [E2E tests](https://github.com/fablo-io/fablo-rest/tree/main/e2e). 275 | -------------------------------------------------------------------------------- /test-network/fablo: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | FABLO_VERSION="1.2.0" 6 | FABLO_IMAGE_NAME="softwaremill/fablo" 7 | FABLO_IMAGE="$FABLO_IMAGE_NAME:$FABLO_VERSION" 8 | 9 | COMMAND="$1" 10 | COMMAND_CALL_ROOT="$(pwd)" 11 | FABLO_TARGET="$COMMAND_CALL_ROOT/fablo-target" 12 | 13 | # Create temporary directory and remove it after script execution 14 | FABLO_TEMP_DIR="$(mktemp -d -t fablo.XXXXXXXX)" 15 | # shellcheck disable=SC2064 16 | trap "rm -rf \"$FABLO_TEMP_DIR\"" EXIT 17 | 18 | getDefaultFabloConfig() { 19 | local fablo_config_json="$COMMAND_CALL_ROOT/fablo-config.json" 20 | local fablo_config_yaml="$COMMAND_CALL_ROOT/fablo-config.yaml" 21 | 22 | if [ -f "$fablo_config_json" ]; then 23 | echo "$fablo_config_json" 24 | elif [ -f "$fablo_config_yaml" ]; then 25 | echo "$fablo_config_yaml" 26 | else 27 | echo "$fablo_config_json" 28 | fi 29 | } 30 | 31 | getSnapshotPath() { 32 | path="${1:-'snapshot'}" 33 | if echo "$path" | grep -q "tar.gz$"; then 34 | echo "$path" 35 | else 36 | echo "$path.fablo.tar.gz" 37 | fi 38 | } 39 | 40 | printSplash() { 41 | darkGray=$'\e[90m' 42 | end=$'\e[0m' 43 | echo "" 44 | echo "┌────── .─. ┌─────. ╷ .────." 45 | echo "│ / \ │ │ │ ╱ ╲ " 46 | echo "├───── / \ ├─────: │ │ │" 47 | echo "│ /───────\ │ │ │ ╲ ╱ " 48 | printf "╵ / \ └─────' └────── '────' %24s\n" "v$FABLO_VERSION" 49 | echo "${darkGray}┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐" 50 | echo "│ https://fablo.io | created at SoftwareMill | backed by Hyperledger Foundation│" 51 | echo "└┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┘${end}" 52 | } 53 | 54 | printHelp() { 55 | printSplash 56 | echo "Usage: 57 | fablo init [node] [rest] [dev] 58 | Creates simple Fablo config in current directory with optional Node.js, chaincode and REST API and dev mode. 59 | 60 | fablo generate [/path/to/fablo-config.json|yaml [/path/to/fablo/target]] 61 | Generates network configuration files in the given directory. Default config file path is '\$(pwd)/fablo-config.json' or '\$(pwd)/fablo-config.yaml', default (and recommended) directory '\$(pwd)/fablo-target'. 62 | 63 | fablo up [/path/to/fablo-config.json|yaml] 64 | Starts the Hyperledger Fabric network for given Fablo configuration file, creates channels, installs and instantiates chaincodes. If there is no configuration, it will call 'generate' command for given config file. 65 | 66 | fablo 67 | Downs, starts or stops the Hyperledger Fabric network for configuration in the current directory. This is similar to down, start and stop commands for Docker Compose. 68 | 69 | fablo reset 70 | Downs and ups the network. Network state is lost, but the configuration is kept intact. 71 | 72 | fablo prune 73 | Downs the network and removes all generated files. 74 | 75 | fablo recreate [/path/to/fablo-config.json|yaml] 76 | Prunes and ups the network. Default config file path is '\$(pwd)/fablo-config.json' or '\$(pwd)/fablo-config.yaml'. 77 | 78 | fablo chaincodes install 79 | Installs all chaincodes on relevant peers. Chaincode directory is specified in Fablo config file. 80 | 81 | fablo chaincode install 82 | Installs chaincode on all relevant peers. Chaincode directory is specified in Fablo config file. 83 | 84 | fablo chaincode upgrade 85 | Upgrades chaincode on all relevant peers. Chaincode directory is specified in Fablo config file. 86 | 87 | fablo channel --help 88 | To list available channel query options which can be executed on running network. 89 | 90 | fablo snapshot 91 | Creates a snapshot of the network in target path. The snapshot contains all network state, including transactions and identities. 92 | 93 | fablo restore 94 | Restores the network from a snapshot. 95 | 96 | fablo use [version] 97 | Updates this Fablo script to specified version. Prints all versions if no version parameter is provided. 98 | 99 | fablo 100 | Prints the manual. 101 | 102 | fablo version [--verbose | -v] 103 | Prints current Fablo version, with optional details." 104 | } 105 | 106 | executeOnFabloDocker() { 107 | local command_with_params="$1" 108 | local fablo_workspace="${2:-$FABLO_TEMP_DIR}" 109 | local fablo_config="$3" 110 | 111 | local fablo_workspace_params=( 112 | -v "$fablo_workspace":/network/workspace 113 | ) 114 | 115 | local fablo_config_params=() 116 | if [ -n "$fablo_config" ]; then 117 | if [ ! -f "$fablo_config" ]; then 118 | echo "File $fablo_config does not exist" 119 | exit 1 120 | fi 121 | 122 | fablo_config="$(cd "$(dirname "$fablo_config")" && pwd)/$(basename "$fablo_config")" 123 | local chaincodes_base_dir="$(dirname "$fablo_config")" 124 | fablo_config_params=( 125 | -v "$fablo_config":/network/fablo-config.json 126 | --env "FABLO_CONFIG=$fablo_config" 127 | --env "CHAINCODES_BASE_DIR=$chaincodes_base_dir" 128 | ) 129 | fi 130 | 131 | docker run -i --rm \ 132 | "${fablo_workspace_params[@]}" \ 133 | "${fablo_config_params[@]}" \ 134 | -u "$(id -u):$(id -g)" \ 135 | $FABLO_IMAGE sh -c "/fablo/docker-entrypoint.sh \"$command_with_params\"" \ 136 | 2>&1 137 | } 138 | 139 | useVersion() { 140 | printSplash 141 | local version="$1" 142 | 143 | if [ -n "$version" ]; then 144 | echo "Updating '$0' to version $version..." 145 | set +e 146 | curl -Lf https://github.com/hyperledger-labs/fablo/releases/download/"$version"/fablo.sh -o "$0" && chmod +x "$0" 147 | else 148 | executeOnFabloDocker "fablo:list-versions" 149 | fi 150 | } 151 | 152 | initConfig() { 153 | printSplash 154 | executeOnFabloDocker "fablo:init $1 $2" 155 | cp -R -i "$FABLO_TEMP_DIR/." "$COMMAND_CALL_ROOT/" 156 | } 157 | 158 | validateConfig() { 159 | local fablo_config=${1:-$(getDefaultFabloConfig)} 160 | executeOnFabloDocker "fablo:validate" "" "$fablo_config" 161 | } 162 | 163 | extendConfig() { 164 | local fablo_config=${1:-$(getDefaultFabloConfig)} 165 | executeOnFabloDocker "fablo:extend-config" "" "$fablo_config" 166 | } 167 | 168 | generateNetworkConfig() { 169 | printSplash 170 | local fablo_config=${1:-$(getDefaultFabloConfig)} 171 | local fablo_target=${2:-$FABLO_TARGET} 172 | 173 | echo "Generating network config" 174 | echo " FABLO_VERSION: $FABLO_VERSION" 175 | echo " FABLO_CONFIG: $fablo_config" 176 | echo " FABLO_NETWORK_ROOT: $fablo_target" 177 | 178 | mkdir -p "$fablo_target" 179 | executeOnFabloDocker "fablo:setup-network" "$fablo_target" "$fablo_config" 180 | ("$fablo_target/hooks/post-generate.sh") 181 | } 182 | 183 | networkPrune() { 184 | if [ -f "$FABLO_TARGET/fabric-docker.sh" ]; then 185 | "$FABLO_TARGET/fabric-docker.sh" down 186 | fi 187 | 188 | if [ -f "$FABLO_TARGET/fabric-k8s.sh" ]; then 189 | "$FABLO_TARGET/fabric-k8s.sh" down 190 | fi 191 | 192 | echo "Removing $FABLO_TARGET" 193 | rm -rf "$FABLO_TARGET" 194 | } 195 | 196 | networkUp() { 197 | if [ ! -d "$FABLO_TARGET" ] || [ -z "$(ls -A "$FABLO_TARGET")" ]; then 198 | echo "Network target directory is empty" 199 | generateNetworkConfig "$1" 200 | fi 201 | executeFabloCommand up 202 | } 203 | 204 | executeFabloCommand() { 205 | if [ ! -d "$FABLO_TARGET" ]; then 206 | echo "Error: This command needs the network to be generated at '$FABLO_TARGET'! Execute 'generate' or 'up' command." 207 | exit 1 208 | fi 209 | 210 | if [ -f "$FABLO_TARGET/fabric-docker.sh" ]; then 211 | echo "Executing Fablo Docker command: $1" 212 | "$FABLO_TARGET/fabric-docker.sh" "$1" "$2" "$3" "$4" "$5" "$6" "$7" "$8" 213 | elif [ -f "$FABLO_TARGET/fabric-k8s.sh" ]; then 214 | echo "Executing Fablo Kubernetes command: $1" 215 | "$FABLO_TARGET/fabric-k8s.sh" "$1" "$2" "$3" "$4" "$5" "$6" "$7" "$8" 216 | else 217 | echo "Error: Corrupted Fablo target directory ($FABLO_TARGET)" 218 | echo "Cannot execute command $1" 219 | exit 1 220 | fi 221 | } 222 | 223 | createSnapshot() { 224 | archive="$(getSnapshotPath "$1")" 225 | echo "Creating network snapshot in '$archive'" 226 | 227 | if [ -f "$archive" ]; then 228 | echo "Error: Snapshot file '$archive' already exists!" 229 | exit 1 230 | fi 231 | 232 | executeFabloCommand snapshot "$FABLO_TEMP_DIR" 233 | (cd "$FABLO_TEMP_DIR" && tar czf tmp.tar.gz *) 234 | mv "$FABLO_TEMP_DIR/tmp.tar.gz" "$archive" 235 | echo "📦 Created snapshot at '$archive'!" 236 | } 237 | 238 | restoreSnapshot() { 239 | archive="$(getSnapshotPath "$1")" 240 | hook_command="$2" 241 | echo "📦 Restoring network from '$archive'" 242 | 243 | if [ ! -f "$archive" ]; then 244 | echo "Fablo snapshot file '$archive' does not exist!" 245 | exit 1 246 | fi 247 | 248 | tar -xf "$archive" -C "$FABLO_TEMP_DIR" 249 | "$FABLO_TEMP_DIR/fablo-target/fabric-docker.sh" clone-to "$COMMAND_CALL_ROOT" "$hook_command" 250 | echo "📦 Network restored from '$archive'! Execute 'start' command to run it." 251 | } 252 | 253 | if [ -z "$COMMAND" ]; then 254 | printHelp 255 | exit 1 256 | 257 | elif [ "$COMMAND" = "help" ] || [ "$COMMAND" = "--help" ]; then 258 | printHelp 259 | 260 | elif [ "$COMMAND" = "version" ]; then 261 | executeOnFabloDocker "fablo:version $2" 262 | 263 | elif [ "$COMMAND" = "use" ]; then 264 | useVersion "$2" 265 | 266 | elif [ "$COMMAND" = "init" ]; then 267 | initConfig "$2" "$3" 268 | 269 | elif [ "$COMMAND" = "validate" ]; then 270 | validateConfig "$2" 271 | 272 | elif [ "$COMMAND" = "extend-config" ]; then 273 | extendConfig "$2" 274 | 275 | elif [ "$COMMAND" = "generate" ]; then 276 | generateNetworkConfig "$2" "$3" 277 | 278 | elif [ "$COMMAND" = "up" ]; then 279 | networkUp "$2" 280 | 281 | elif [ "$COMMAND" = "prune" ]; then 282 | networkPrune 283 | 284 | elif [ "$COMMAND" = "recreate" ]; then 285 | networkPrune 286 | networkUp "$2" 287 | 288 | elif [ "$COMMAND" = "snapshot" ]; then 289 | createSnapshot "$2" 290 | 291 | elif [ "$COMMAND" = "restore" ]; then 292 | restoreSnapshot "$2" "${3:-""}" 293 | 294 | else 295 | executeFabloCommand "$COMMAND" "$2" "$3" "$4" "$5" "$6" "$7" "$8" 296 | fi 297 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /test-network/chaincode-kv-node/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chaincode-kv-node", 3 | "version": "0.2.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "chaincode-kv-node", 9 | "version": "0.2.0", 10 | "dependencies": { 11 | "fabric-contract-api": "2.4.2", 12 | "fabric-shim": "2.4.2" 13 | }, 14 | "engines": { 15 | "node": ">=8", 16 | "npm": ">=5" 17 | } 18 | }, 19 | "node_modules/@colors/colors": { 20 | "version": "1.5.0", 21 | "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", 22 | "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", 23 | "engines": { 24 | "node": ">=0.1.90" 25 | } 26 | }, 27 | "node_modules/@dabh/diagnostics": { 28 | "version": "2.0.3", 29 | "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", 30 | "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", 31 | "dependencies": { 32 | "colorspace": "1.1.x", 33 | "enabled": "2.0.x", 34 | "kuler": "^2.0.0" 35 | } 36 | }, 37 | "node_modules/@fidm/asn1": { 38 | "version": "1.0.4", 39 | "resolved": "https://registry.npmjs.org/@fidm/asn1/-/asn1-1.0.4.tgz", 40 | "integrity": "sha512-esd1jyNvRb2HVaQGq2Gg8Z0kbQPXzV9Tq5Z14KNIov6KfFD6PTaRIO8UpcsYiTNzOqJpmyzWgVTrUwFV3UF4TQ==", 41 | "engines": { 42 | "node": ">= 8" 43 | } 44 | }, 45 | "node_modules/@fidm/x509": { 46 | "version": "1.2.1", 47 | "resolved": "https://registry.npmjs.org/@fidm/x509/-/x509-1.2.1.tgz", 48 | "integrity": "sha512-nwc2iesjyc9hkuzcrMCBXQRn653XuAUKorfWM8PZyJawiy1QzLj4vahwzaI25+pfpwOLvMzbJ0uKpWLDNmo16w==", 49 | "dependencies": { 50 | "@fidm/asn1": "^1.0.4", 51 | "tweetnacl": "^1.0.1" 52 | }, 53 | "engines": { 54 | "node": ">= 8" 55 | } 56 | }, 57 | "node_modules/@grpc/grpc-js": { 58 | "version": "1.11.2", 59 | "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.11.2.tgz", 60 | "integrity": "sha512-DWp92gDD7/Qkj7r8kus6/HCINeo3yPZWZ3paKgDgsbKbSpoxKg1yvN8xe2Q8uE3zOsPe3bX8FQX2+XValq2yTw==", 61 | "dependencies": { 62 | "@grpc/proto-loader": "^0.7.13", 63 | "@js-sdsl/ordered-map": "^4.4.2" 64 | }, 65 | "engines": { 66 | "node": ">=12.10.0" 67 | } 68 | }, 69 | "node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": { 70 | "version": "0.7.13", 71 | "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz", 72 | "integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==", 73 | "dependencies": { 74 | "lodash.camelcase": "^4.3.0", 75 | "long": "^5.0.0", 76 | "protobufjs": "^7.2.5", 77 | "yargs": "^17.7.2" 78 | }, 79 | "bin": { 80 | "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" 81 | }, 82 | "engines": { 83 | "node": ">=6" 84 | } 85 | }, 86 | "node_modules/@grpc/grpc-js/node_modules/long": { 87 | "version": "5.2.3", 88 | "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", 89 | "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" 90 | }, 91 | "node_modules/@grpc/grpc-js/node_modules/protobufjs": { 92 | "version": "7.4.0", 93 | "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", 94 | "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", 95 | "hasInstallScript": true, 96 | "dependencies": { 97 | "@protobufjs/aspromise": "^1.1.2", 98 | "@protobufjs/base64": "^1.1.2", 99 | "@protobufjs/codegen": "^2.0.4", 100 | "@protobufjs/eventemitter": "^1.1.0", 101 | "@protobufjs/fetch": "^1.1.0", 102 | "@protobufjs/float": "^1.0.2", 103 | "@protobufjs/inquire": "^1.1.0", 104 | "@protobufjs/path": "^1.1.2", 105 | "@protobufjs/pool": "^1.1.0", 106 | "@protobufjs/utf8": "^1.1.0", 107 | "@types/node": ">=13.7.0", 108 | "long": "^5.0.0" 109 | }, 110 | "engines": { 111 | "node": ">=12.0.0" 112 | } 113 | }, 114 | "node_modules/@grpc/proto-loader": { 115 | "version": "0.6.13", 116 | "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.13.tgz", 117 | "integrity": "sha512-FjxPYDRTn6Ec3V0arm1FtSpmP6V50wuph2yILpyvTKzjc76oDdoihXqM1DzOW5ubvCC8GivfCnNtfaRE8myJ7g==", 118 | "dependencies": { 119 | "@types/long": "^4.0.1", 120 | "lodash.camelcase": "^4.3.0", 121 | "long": "^4.0.0", 122 | "protobufjs": "^6.11.3", 123 | "yargs": "^16.2.0" 124 | }, 125 | "bin": { 126 | "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" 127 | }, 128 | "engines": { 129 | "node": ">=6" 130 | } 131 | }, 132 | "node_modules/@grpc/proto-loader/node_modules/cliui": { 133 | "version": "7.0.4", 134 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", 135 | "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", 136 | "dependencies": { 137 | "string-width": "^4.2.0", 138 | "strip-ansi": "^6.0.0", 139 | "wrap-ansi": "^7.0.0" 140 | } 141 | }, 142 | "node_modules/@grpc/proto-loader/node_modules/yargs": { 143 | "version": "16.2.0", 144 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", 145 | "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", 146 | "dependencies": { 147 | "cliui": "^7.0.2", 148 | "escalade": "^3.1.1", 149 | "get-caller-file": "^2.0.5", 150 | "require-directory": "^2.1.1", 151 | "string-width": "^4.2.0", 152 | "y18n": "^5.0.5", 153 | "yargs-parser": "^20.2.2" 154 | }, 155 | "engines": { 156 | "node": ">=10" 157 | } 158 | }, 159 | "node_modules/@grpc/proto-loader/node_modules/yargs-parser": { 160 | "version": "20.2.9", 161 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", 162 | "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", 163 | "engines": { 164 | "node": ">=10" 165 | } 166 | }, 167 | "node_modules/@js-sdsl/ordered-map": { 168 | "version": "4.4.2", 169 | "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", 170 | "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", 171 | "funding": { 172 | "type": "opencollective", 173 | "url": "https://opencollective.com/js-sdsl" 174 | } 175 | }, 176 | "node_modules/@protobufjs/aspromise": { 177 | "version": "1.1.2", 178 | "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", 179 | "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" 180 | }, 181 | "node_modules/@protobufjs/base64": { 182 | "version": "1.1.2", 183 | "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", 184 | "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" 185 | }, 186 | "node_modules/@protobufjs/codegen": { 187 | "version": "2.0.4", 188 | "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", 189 | "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" 190 | }, 191 | "node_modules/@protobufjs/eventemitter": { 192 | "version": "1.1.0", 193 | "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", 194 | "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" 195 | }, 196 | "node_modules/@protobufjs/fetch": { 197 | "version": "1.1.0", 198 | "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", 199 | "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", 200 | "dependencies": { 201 | "@protobufjs/aspromise": "^1.1.1", 202 | "@protobufjs/inquire": "^1.1.0" 203 | } 204 | }, 205 | "node_modules/@protobufjs/float": { 206 | "version": "1.0.2", 207 | "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", 208 | "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" 209 | }, 210 | "node_modules/@protobufjs/inquire": { 211 | "version": "1.1.0", 212 | "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", 213 | "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" 214 | }, 215 | "node_modules/@protobufjs/path": { 216 | "version": "1.1.2", 217 | "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", 218 | "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" 219 | }, 220 | "node_modules/@protobufjs/pool": { 221 | "version": "1.1.0", 222 | "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", 223 | "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" 224 | }, 225 | "node_modules/@protobufjs/utf8": { 226 | "version": "1.1.0", 227 | "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", 228 | "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" 229 | }, 230 | "node_modules/@types/long": { 231 | "version": "4.0.2", 232 | "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", 233 | "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==" 234 | }, 235 | "node_modules/@types/node": { 236 | "version": "16.18.36", 237 | "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.36.tgz", 238 | "integrity": "sha512-8egDX8dE50XyXWH6C6PRCNkTP106DuUrvdrednFouDSmCi7IOvrqr0frznfZaHifHH/3aq/7a7v9N4wdXMqhBQ==" 239 | }, 240 | "node_modules/@types/triple-beam": { 241 | "version": "1.3.2", 242 | "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.2.tgz", 243 | "integrity": "sha512-txGIh+0eDFzKGC25zORnswy+br1Ha7hj5cMVwKIU7+s0U2AxxJru/jZSMU6OC9MJWP6+pc/hc6ZjyZShpsyY2g==" 244 | }, 245 | "node_modules/ajv": { 246 | "version": "6.12.6", 247 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", 248 | "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", 249 | "dependencies": { 250 | "fast-deep-equal": "^3.1.1", 251 | "fast-json-stable-stringify": "^2.0.0", 252 | "json-schema-traverse": "^0.4.1", 253 | "uri-js": "^4.2.2" 254 | }, 255 | "funding": { 256 | "type": "github", 257 | "url": "https://github.com/sponsors/epoberezkin" 258 | } 259 | }, 260 | "node_modules/ansi-regex": { 261 | "version": "5.0.1", 262 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 263 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 264 | "engines": { 265 | "node": ">=8" 266 | } 267 | }, 268 | "node_modules/ansi-styles": { 269 | "version": "4.3.0", 270 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 271 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 272 | "dependencies": { 273 | "color-convert": "^2.0.1" 274 | }, 275 | "engines": { 276 | "node": ">=8" 277 | }, 278 | "funding": { 279 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 280 | } 281 | }, 282 | "node_modules/ansi-styles/node_modules/color-convert": { 283 | "version": "2.0.1", 284 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 285 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 286 | "dependencies": { 287 | "color-name": "~1.1.4" 288 | }, 289 | "engines": { 290 | "node": ">=7.0.0" 291 | } 292 | }, 293 | "node_modules/ansi-styles/node_modules/color-name": { 294 | "version": "1.1.4", 295 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 296 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" 297 | }, 298 | "node_modules/async": { 299 | "version": "3.2.4", 300 | "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", 301 | "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" 302 | }, 303 | "node_modules/class-transformer": { 304 | "version": "0.4.0", 305 | "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.4.0.tgz", 306 | "integrity": "sha512-ETWD/H2TbWbKEi7m9N4Km5+cw1hNcqJSxlSYhsLsNjQzWWiZIYA1zafxpK9PwVfaZ6AqR5rrjPVUBGESm5tQUA==" 307 | }, 308 | "node_modules/cliui": { 309 | "version": "8.0.1", 310 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", 311 | "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", 312 | "dependencies": { 313 | "string-width": "^4.2.0", 314 | "strip-ansi": "^6.0.1", 315 | "wrap-ansi": "^7.0.0" 316 | }, 317 | "engines": { 318 | "node": ">=12" 319 | } 320 | }, 321 | "node_modules/color": { 322 | "version": "3.2.1", 323 | "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", 324 | "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", 325 | "dependencies": { 326 | "color-convert": "^1.9.3", 327 | "color-string": "^1.6.0" 328 | } 329 | }, 330 | "node_modules/color-convert": { 331 | "version": "1.9.3", 332 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 333 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 334 | "dependencies": { 335 | "color-name": "1.1.3" 336 | } 337 | }, 338 | "node_modules/color-name": { 339 | "version": "1.1.3", 340 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 341 | "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" 342 | }, 343 | "node_modules/color-string": { 344 | "version": "1.9.1", 345 | "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", 346 | "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", 347 | "dependencies": { 348 | "color-name": "^1.0.0", 349 | "simple-swizzle": "^0.2.2" 350 | } 351 | }, 352 | "node_modules/colorspace": { 353 | "version": "1.1.4", 354 | "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", 355 | "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", 356 | "dependencies": { 357 | "color": "^3.1.3", 358 | "text-hex": "1.0.x" 359 | } 360 | }, 361 | "node_modules/emoji-regex": { 362 | "version": "8.0.0", 363 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 364 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" 365 | }, 366 | "node_modules/enabled": { 367 | "version": "2.0.0", 368 | "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", 369 | "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" 370 | }, 371 | "node_modules/escalade": { 372 | "version": "3.1.1", 373 | "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", 374 | "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", 375 | "engines": { 376 | "node": ">=6" 377 | } 378 | }, 379 | "node_modules/fabric-contract-api": { 380 | "version": "2.4.2", 381 | "resolved": "https://registry.npmjs.org/fabric-contract-api/-/fabric-contract-api-2.4.2.tgz", 382 | "integrity": "sha512-19lInYwkx9gDXL7BTyk0lfNxO4w4WyqFTIxCiQRTpEQVoolLBDyvtuDpBHDOX4dHMA5YltzGgcxRsBhBAaLTiA==", 383 | "dependencies": { 384 | "class-transformer": "^0.4.0", 385 | "fabric-shim-api": "2.4.2", 386 | "fast-safe-stringify": "^2.1.1", 387 | "get-params": "^0.1.2", 388 | "reflect-metadata": "^0.1.13", 389 | "winston": "^3.7.2" 390 | }, 391 | "engines": { 392 | "node": "^16.4.0", 393 | "npm": "^8.0.0" 394 | } 395 | }, 396 | "node_modules/fabric-shim": { 397 | "version": "2.4.2", 398 | "resolved": "https://registry.npmjs.org/fabric-shim/-/fabric-shim-2.4.2.tgz", 399 | "integrity": "sha512-NAl+YMeHVwUiFKgVF8ViKA2o5ZEz89fBZmuedcUE1T13nP31XZ5mFYauyhwu83VyLhh+5LdypkibSZ+aMilQ5g==", 400 | "dependencies": { 401 | "@fidm/x509": "^1.2.1", 402 | "@grpc/grpc-js": "^1.4.1", 403 | "@grpc/proto-loader": "^0.6.6", 404 | "@types/node": "^16.11.1", 405 | "ajv": "^6.12.2", 406 | "fabric-contract-api": "2.4.2", 407 | "fabric-shim-api": "2.4.2", 408 | "fs-extra": "^10.0.1", 409 | "reflect-metadata": "^0.1.13", 410 | "winston": "^3.7.2", 411 | "yargs": "^17.4.0", 412 | "yargs-parser": "^21.0.1" 413 | }, 414 | "bin": { 415 | "fabric-chaincode-node": "cli.js" 416 | }, 417 | "engines": { 418 | "node": "^16.4.0", 419 | "npm": "^8.0.0" 420 | } 421 | }, 422 | "node_modules/fabric-shim-api": { 423 | "version": "2.4.2", 424 | "resolved": "https://registry.npmjs.org/fabric-shim-api/-/fabric-shim-api-2.4.2.tgz", 425 | "integrity": "sha512-ODjKxDsf6KvN3AhHhztbVSr94MjtTr+Mz/S1SinAbxVPX1yAEJ/ONEoezyu7rLslcr5zJxF1ei2z4Bp08nB2zw==", 426 | "engines": { 427 | "eslint": "^6.6.0", 428 | "node": "^16.4.0", 429 | "npm": "^8.0.0" 430 | } 431 | }, 432 | "node_modules/fast-deep-equal": { 433 | "version": "3.1.3", 434 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", 435 | "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" 436 | }, 437 | "node_modules/fast-json-stable-stringify": { 438 | "version": "2.1.0", 439 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", 440 | "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" 441 | }, 442 | "node_modules/fast-safe-stringify": { 443 | "version": "2.1.1", 444 | "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", 445 | "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" 446 | }, 447 | "node_modules/fecha": { 448 | "version": "4.2.3", 449 | "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", 450 | "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" 451 | }, 452 | "node_modules/fn.name": { 453 | "version": "1.1.0", 454 | "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", 455 | "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" 456 | }, 457 | "node_modules/fs-extra": { 458 | "version": "10.1.0", 459 | "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", 460 | "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", 461 | "dependencies": { 462 | "graceful-fs": "^4.2.0", 463 | "jsonfile": "^6.0.1", 464 | "universalify": "^2.0.0" 465 | }, 466 | "engines": { 467 | "node": ">=12" 468 | } 469 | }, 470 | "node_modules/get-caller-file": { 471 | "version": "2.0.5", 472 | "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", 473 | "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", 474 | "engines": { 475 | "node": "6.* || 8.* || >= 10.*" 476 | } 477 | }, 478 | "node_modules/get-params": { 479 | "version": "0.1.2", 480 | "resolved": "https://registry.npmjs.org/get-params/-/get-params-0.1.2.tgz", 481 | "integrity": "sha512-41eOxtlGgHQRbFyA8KTH+w+32Em3cRdfBud7j67ulzmIfmaHX9doq47s0fa4P5o9H64BZX9nrYI6sJvk46Op+Q==" 482 | }, 483 | "node_modules/graceful-fs": { 484 | "version": "4.2.11", 485 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", 486 | "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" 487 | }, 488 | "node_modules/inherits": { 489 | "version": "2.0.4", 490 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 491 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 492 | }, 493 | "node_modules/is-arrayish": { 494 | "version": "0.3.2", 495 | "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", 496 | "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" 497 | }, 498 | "node_modules/is-fullwidth-code-point": { 499 | "version": "3.0.0", 500 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 501 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", 502 | "engines": { 503 | "node": ">=8" 504 | } 505 | }, 506 | "node_modules/is-stream": { 507 | "version": "2.0.1", 508 | "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", 509 | "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", 510 | "engines": { 511 | "node": ">=8" 512 | }, 513 | "funding": { 514 | "url": "https://github.com/sponsors/sindresorhus" 515 | } 516 | }, 517 | "node_modules/json-schema-traverse": { 518 | "version": "0.4.1", 519 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 520 | "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" 521 | }, 522 | "node_modules/jsonfile": { 523 | "version": "6.1.0", 524 | "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", 525 | "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", 526 | "dependencies": { 527 | "universalify": "^2.0.0" 528 | }, 529 | "optionalDependencies": { 530 | "graceful-fs": "^4.1.6" 531 | } 532 | }, 533 | "node_modules/kuler": { 534 | "version": "2.0.0", 535 | "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", 536 | "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" 537 | }, 538 | "node_modules/lodash.camelcase": { 539 | "version": "4.3.0", 540 | "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", 541 | "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" 542 | }, 543 | "node_modules/logform": { 544 | "version": "2.5.1", 545 | "resolved": "https://registry.npmjs.org/logform/-/logform-2.5.1.tgz", 546 | "integrity": "sha512-9FyqAm9o9NKKfiAKfZoYo9bGXXuwMkxQiQttkT4YjjVtQVIQtK6LmVtlxmCaFswo6N4AfEkHqZTV0taDtPotNg==", 547 | "dependencies": { 548 | "@colors/colors": "1.5.0", 549 | "@types/triple-beam": "^1.3.2", 550 | "fecha": "^4.2.0", 551 | "ms": "^2.1.1", 552 | "safe-stable-stringify": "^2.3.1", 553 | "triple-beam": "^1.3.0" 554 | } 555 | }, 556 | "node_modules/long": { 557 | "version": "4.0.0", 558 | "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", 559 | "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" 560 | }, 561 | "node_modules/ms": { 562 | "version": "2.1.3", 563 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 564 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 565 | }, 566 | "node_modules/one-time": { 567 | "version": "1.0.0", 568 | "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", 569 | "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", 570 | "dependencies": { 571 | "fn.name": "1.x.x" 572 | } 573 | }, 574 | "node_modules/protobufjs": { 575 | "version": "6.11.4", 576 | "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.4.tgz", 577 | "integrity": "sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==", 578 | "hasInstallScript": true, 579 | "dependencies": { 580 | "@protobufjs/aspromise": "^1.1.2", 581 | "@protobufjs/base64": "^1.1.2", 582 | "@protobufjs/codegen": "^2.0.4", 583 | "@protobufjs/eventemitter": "^1.1.0", 584 | "@protobufjs/fetch": "^1.1.0", 585 | "@protobufjs/float": "^1.0.2", 586 | "@protobufjs/inquire": "^1.1.0", 587 | "@protobufjs/path": "^1.1.2", 588 | "@protobufjs/pool": "^1.1.0", 589 | "@protobufjs/utf8": "^1.1.0", 590 | "@types/long": "^4.0.1", 591 | "@types/node": ">=13.7.0", 592 | "long": "^4.0.0" 593 | }, 594 | "bin": { 595 | "pbjs": "bin/pbjs", 596 | "pbts": "bin/pbts" 597 | } 598 | }, 599 | "node_modules/punycode": { 600 | "version": "2.3.0", 601 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", 602 | "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", 603 | "engines": { 604 | "node": ">=6" 605 | } 606 | }, 607 | "node_modules/readable-stream": { 608 | "version": "3.6.2", 609 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", 610 | "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", 611 | "dependencies": { 612 | "inherits": "^2.0.3", 613 | "string_decoder": "^1.1.1", 614 | "util-deprecate": "^1.0.1" 615 | }, 616 | "engines": { 617 | "node": ">= 6" 618 | } 619 | }, 620 | "node_modules/reflect-metadata": { 621 | "version": "0.1.13", 622 | "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", 623 | "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==" 624 | }, 625 | "node_modules/require-directory": { 626 | "version": "2.1.1", 627 | "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", 628 | "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", 629 | "engines": { 630 | "node": ">=0.10.0" 631 | } 632 | }, 633 | "node_modules/safe-buffer": { 634 | "version": "5.2.1", 635 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 636 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 637 | "funding": [ 638 | { 639 | "type": "github", 640 | "url": "https://github.com/sponsors/feross" 641 | }, 642 | { 643 | "type": "patreon", 644 | "url": "https://www.patreon.com/feross" 645 | }, 646 | { 647 | "type": "consulting", 648 | "url": "https://feross.org/support" 649 | } 650 | ] 651 | }, 652 | "node_modules/safe-stable-stringify": { 653 | "version": "2.4.3", 654 | "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", 655 | "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==", 656 | "engines": { 657 | "node": ">=10" 658 | } 659 | }, 660 | "node_modules/simple-swizzle": { 661 | "version": "0.2.2", 662 | "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", 663 | "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", 664 | "dependencies": { 665 | "is-arrayish": "^0.3.1" 666 | } 667 | }, 668 | "node_modules/stack-trace": { 669 | "version": "0.0.10", 670 | "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", 671 | "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", 672 | "engines": { 673 | "node": "*" 674 | } 675 | }, 676 | "node_modules/string_decoder": { 677 | "version": "1.3.0", 678 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", 679 | "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", 680 | "dependencies": { 681 | "safe-buffer": "~5.2.0" 682 | } 683 | }, 684 | "node_modules/string-width": { 685 | "version": "4.2.3", 686 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 687 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 688 | "dependencies": { 689 | "emoji-regex": "^8.0.0", 690 | "is-fullwidth-code-point": "^3.0.0", 691 | "strip-ansi": "^6.0.1" 692 | }, 693 | "engines": { 694 | "node": ">=8" 695 | } 696 | }, 697 | "node_modules/strip-ansi": { 698 | "version": "6.0.1", 699 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 700 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 701 | "dependencies": { 702 | "ansi-regex": "^5.0.1" 703 | }, 704 | "engines": { 705 | "node": ">=8" 706 | } 707 | }, 708 | "node_modules/text-hex": { 709 | "version": "1.0.0", 710 | "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", 711 | "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" 712 | }, 713 | "node_modules/triple-beam": { 714 | "version": "1.3.0", 715 | "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", 716 | "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==" 717 | }, 718 | "node_modules/tweetnacl": { 719 | "version": "1.0.3", 720 | "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", 721 | "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==" 722 | }, 723 | "node_modules/universalify": { 724 | "version": "2.0.0", 725 | "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", 726 | "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", 727 | "engines": { 728 | "node": ">= 10.0.0" 729 | } 730 | }, 731 | "node_modules/uri-js": { 732 | "version": "4.4.1", 733 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", 734 | "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", 735 | "dependencies": { 736 | "punycode": "^2.1.0" 737 | } 738 | }, 739 | "node_modules/util-deprecate": { 740 | "version": "1.0.2", 741 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 742 | "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" 743 | }, 744 | "node_modules/winston": { 745 | "version": "3.9.0", 746 | "resolved": "https://registry.npmjs.org/winston/-/winston-3.9.0.tgz", 747 | "integrity": "sha512-jW51iW/X95BCW6MMtZWr2jKQBP4hV5bIDq9QrIjfDk6Q9QuxvTKEAlpUNAzP+HYHFFCeENhph16s0zEunu4uuQ==", 748 | "dependencies": { 749 | "@colors/colors": "1.5.0", 750 | "@dabh/diagnostics": "^2.0.2", 751 | "async": "^3.2.3", 752 | "is-stream": "^2.0.0", 753 | "logform": "^2.4.0", 754 | "one-time": "^1.0.0", 755 | "readable-stream": "^3.4.0", 756 | "safe-stable-stringify": "^2.3.1", 757 | "stack-trace": "0.0.x", 758 | "triple-beam": "^1.3.0", 759 | "winston-transport": "^4.5.0" 760 | }, 761 | "engines": { 762 | "node": ">= 12.0.0" 763 | } 764 | }, 765 | "node_modules/winston-transport": { 766 | "version": "4.5.0", 767 | "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.5.0.tgz", 768 | "integrity": "sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q==", 769 | "dependencies": { 770 | "logform": "^2.3.2", 771 | "readable-stream": "^3.6.0", 772 | "triple-beam": "^1.3.0" 773 | }, 774 | "engines": { 775 | "node": ">= 6.4.0" 776 | } 777 | }, 778 | "node_modules/wrap-ansi": { 779 | "version": "7.0.0", 780 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", 781 | "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", 782 | "dependencies": { 783 | "ansi-styles": "^4.0.0", 784 | "string-width": "^4.1.0", 785 | "strip-ansi": "^6.0.0" 786 | }, 787 | "engines": { 788 | "node": ">=10" 789 | }, 790 | "funding": { 791 | "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 792 | } 793 | }, 794 | "node_modules/y18n": { 795 | "version": "5.0.8", 796 | "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", 797 | "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", 798 | "engines": { 799 | "node": ">=10" 800 | } 801 | }, 802 | "node_modules/yargs": { 803 | "version": "17.7.2", 804 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", 805 | "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", 806 | "dependencies": { 807 | "cliui": "^8.0.1", 808 | "escalade": "^3.1.1", 809 | "get-caller-file": "^2.0.5", 810 | "require-directory": "^2.1.1", 811 | "string-width": "^4.2.3", 812 | "y18n": "^5.0.5", 813 | "yargs-parser": "^21.1.1" 814 | }, 815 | "engines": { 816 | "node": ">=12" 817 | } 818 | }, 819 | "node_modules/yargs-parser": { 820 | "version": "21.1.1", 821 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", 822 | "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", 823 | "engines": { 824 | "node": ">=12" 825 | } 826 | } 827 | }, 828 | "dependencies": { 829 | "@colors/colors": { 830 | "version": "1.5.0", 831 | "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", 832 | "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==" 833 | }, 834 | "@dabh/diagnostics": { 835 | "version": "2.0.3", 836 | "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", 837 | "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", 838 | "requires": { 839 | "colorspace": "1.1.x", 840 | "enabled": "2.0.x", 841 | "kuler": "^2.0.0" 842 | } 843 | }, 844 | "@fidm/asn1": { 845 | "version": "1.0.4", 846 | "resolved": "https://registry.npmjs.org/@fidm/asn1/-/asn1-1.0.4.tgz", 847 | "integrity": "sha512-esd1jyNvRb2HVaQGq2Gg8Z0kbQPXzV9Tq5Z14KNIov6KfFD6PTaRIO8UpcsYiTNzOqJpmyzWgVTrUwFV3UF4TQ==" 848 | }, 849 | "@fidm/x509": { 850 | "version": "1.2.1", 851 | "resolved": "https://registry.npmjs.org/@fidm/x509/-/x509-1.2.1.tgz", 852 | "integrity": "sha512-nwc2iesjyc9hkuzcrMCBXQRn653XuAUKorfWM8PZyJawiy1QzLj4vahwzaI25+pfpwOLvMzbJ0uKpWLDNmo16w==", 853 | "requires": { 854 | "@fidm/asn1": "^1.0.4", 855 | "tweetnacl": "^1.0.1" 856 | } 857 | }, 858 | "@grpc/grpc-js": { 859 | "version": "1.11.2", 860 | "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.11.2.tgz", 861 | "integrity": "sha512-DWp92gDD7/Qkj7r8kus6/HCINeo3yPZWZ3paKgDgsbKbSpoxKg1yvN8xe2Q8uE3zOsPe3bX8FQX2+XValq2yTw==", 862 | "requires": { 863 | "@grpc/proto-loader": "^0.7.13", 864 | "@js-sdsl/ordered-map": "^4.4.2" 865 | }, 866 | "dependencies": { 867 | "@grpc/proto-loader": { 868 | "version": "0.7.13", 869 | "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz", 870 | "integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==", 871 | "requires": { 872 | "lodash.camelcase": "^4.3.0", 873 | "long": "^5.0.0", 874 | "protobufjs": "^7.2.5", 875 | "yargs": "^17.7.2" 876 | } 877 | }, 878 | "long": { 879 | "version": "5.2.3", 880 | "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", 881 | "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" 882 | }, 883 | "protobufjs": { 884 | "version": "7.4.0", 885 | "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", 886 | "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", 887 | "requires": { 888 | "@protobufjs/aspromise": "^1.1.2", 889 | "@protobufjs/base64": "^1.1.2", 890 | "@protobufjs/codegen": "^2.0.4", 891 | "@protobufjs/eventemitter": "^1.1.0", 892 | "@protobufjs/fetch": "^1.1.0", 893 | "@protobufjs/float": "^1.0.2", 894 | "@protobufjs/inquire": "^1.1.0", 895 | "@protobufjs/path": "^1.1.2", 896 | "@protobufjs/pool": "^1.1.0", 897 | "@protobufjs/utf8": "^1.1.0", 898 | "@types/node": ">=13.7.0", 899 | "long": "^5.0.0" 900 | } 901 | } 902 | } 903 | }, 904 | "@grpc/proto-loader": { 905 | "version": "0.6.13", 906 | "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.13.tgz", 907 | "integrity": "sha512-FjxPYDRTn6Ec3V0arm1FtSpmP6V50wuph2yILpyvTKzjc76oDdoihXqM1DzOW5ubvCC8GivfCnNtfaRE8myJ7g==", 908 | "requires": { 909 | "@types/long": "^4.0.1", 910 | "lodash.camelcase": "^4.3.0", 911 | "long": "^4.0.0", 912 | "protobufjs": "^6.11.3", 913 | "yargs": "^16.2.0" 914 | }, 915 | "dependencies": { 916 | "cliui": { 917 | "version": "7.0.4", 918 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", 919 | "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", 920 | "requires": { 921 | "string-width": "^4.2.0", 922 | "strip-ansi": "^6.0.0", 923 | "wrap-ansi": "^7.0.0" 924 | } 925 | }, 926 | "yargs": { 927 | "version": "16.2.0", 928 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", 929 | "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", 930 | "requires": { 931 | "cliui": "^7.0.2", 932 | "escalade": "^3.1.1", 933 | "get-caller-file": "^2.0.5", 934 | "require-directory": "^2.1.1", 935 | "string-width": "^4.2.0", 936 | "y18n": "^5.0.5", 937 | "yargs-parser": "^20.2.2" 938 | } 939 | }, 940 | "yargs-parser": { 941 | "version": "20.2.9", 942 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", 943 | "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==" 944 | } 945 | } 946 | }, 947 | "@js-sdsl/ordered-map": { 948 | "version": "4.4.2", 949 | "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", 950 | "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==" 951 | }, 952 | "@protobufjs/aspromise": { 953 | "version": "1.1.2", 954 | "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", 955 | "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" 956 | }, 957 | "@protobufjs/base64": { 958 | "version": "1.1.2", 959 | "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", 960 | "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" 961 | }, 962 | "@protobufjs/codegen": { 963 | "version": "2.0.4", 964 | "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", 965 | "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" 966 | }, 967 | "@protobufjs/eventemitter": { 968 | "version": "1.1.0", 969 | "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", 970 | "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" 971 | }, 972 | "@protobufjs/fetch": { 973 | "version": "1.1.0", 974 | "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", 975 | "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", 976 | "requires": { 977 | "@protobufjs/aspromise": "^1.1.1", 978 | "@protobufjs/inquire": "^1.1.0" 979 | } 980 | }, 981 | "@protobufjs/float": { 982 | "version": "1.0.2", 983 | "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", 984 | "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" 985 | }, 986 | "@protobufjs/inquire": { 987 | "version": "1.1.0", 988 | "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", 989 | "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" 990 | }, 991 | "@protobufjs/path": { 992 | "version": "1.1.2", 993 | "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", 994 | "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" 995 | }, 996 | "@protobufjs/pool": { 997 | "version": "1.1.0", 998 | "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", 999 | "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" 1000 | }, 1001 | "@protobufjs/utf8": { 1002 | "version": "1.1.0", 1003 | "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", 1004 | "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" 1005 | }, 1006 | "@types/long": { 1007 | "version": "4.0.2", 1008 | "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", 1009 | "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==" 1010 | }, 1011 | "@types/node": { 1012 | "version": "16.18.36", 1013 | "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.36.tgz", 1014 | "integrity": "sha512-8egDX8dE50XyXWH6C6PRCNkTP106DuUrvdrednFouDSmCi7IOvrqr0frznfZaHifHH/3aq/7a7v9N4wdXMqhBQ==" 1015 | }, 1016 | "@types/triple-beam": { 1017 | "version": "1.3.2", 1018 | "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.2.tgz", 1019 | "integrity": "sha512-txGIh+0eDFzKGC25zORnswy+br1Ha7hj5cMVwKIU7+s0U2AxxJru/jZSMU6OC9MJWP6+pc/hc6ZjyZShpsyY2g==" 1020 | }, 1021 | "ajv": { 1022 | "version": "6.12.6", 1023 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", 1024 | "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", 1025 | "requires": { 1026 | "fast-deep-equal": "^3.1.1", 1027 | "fast-json-stable-stringify": "^2.0.0", 1028 | "json-schema-traverse": "^0.4.1", 1029 | "uri-js": "^4.2.2" 1030 | } 1031 | }, 1032 | "ansi-regex": { 1033 | "version": "5.0.1", 1034 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 1035 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" 1036 | }, 1037 | "ansi-styles": { 1038 | "version": "4.3.0", 1039 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 1040 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 1041 | "requires": { 1042 | "color-convert": "^2.0.1" 1043 | }, 1044 | "dependencies": { 1045 | "color-convert": { 1046 | "version": "2.0.1", 1047 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 1048 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 1049 | "requires": { 1050 | "color-name": "~1.1.4" 1051 | } 1052 | }, 1053 | "color-name": { 1054 | "version": "1.1.4", 1055 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 1056 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" 1057 | } 1058 | } 1059 | }, 1060 | "async": { 1061 | "version": "3.2.4", 1062 | "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", 1063 | "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" 1064 | }, 1065 | "class-transformer": { 1066 | "version": "0.4.0", 1067 | "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.4.0.tgz", 1068 | "integrity": "sha512-ETWD/H2TbWbKEi7m9N4Km5+cw1hNcqJSxlSYhsLsNjQzWWiZIYA1zafxpK9PwVfaZ6AqR5rrjPVUBGESm5tQUA==" 1069 | }, 1070 | "cliui": { 1071 | "version": "8.0.1", 1072 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", 1073 | "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", 1074 | "requires": { 1075 | "string-width": "^4.2.0", 1076 | "strip-ansi": "^6.0.1", 1077 | "wrap-ansi": "^7.0.0" 1078 | } 1079 | }, 1080 | "color": { 1081 | "version": "3.2.1", 1082 | "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", 1083 | "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", 1084 | "requires": { 1085 | "color-convert": "^1.9.3", 1086 | "color-string": "^1.6.0" 1087 | } 1088 | }, 1089 | "color-convert": { 1090 | "version": "1.9.3", 1091 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 1092 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 1093 | "requires": { 1094 | "color-name": "1.1.3" 1095 | } 1096 | }, 1097 | "color-name": { 1098 | "version": "1.1.3", 1099 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 1100 | "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" 1101 | }, 1102 | "color-string": { 1103 | "version": "1.9.1", 1104 | "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", 1105 | "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", 1106 | "requires": { 1107 | "color-name": "^1.0.0", 1108 | "simple-swizzle": "^0.2.2" 1109 | } 1110 | }, 1111 | "colorspace": { 1112 | "version": "1.1.4", 1113 | "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", 1114 | "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", 1115 | "requires": { 1116 | "color": "^3.1.3", 1117 | "text-hex": "1.0.x" 1118 | } 1119 | }, 1120 | "emoji-regex": { 1121 | "version": "8.0.0", 1122 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 1123 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" 1124 | }, 1125 | "enabled": { 1126 | "version": "2.0.0", 1127 | "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", 1128 | "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" 1129 | }, 1130 | "escalade": { 1131 | "version": "3.1.1", 1132 | "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", 1133 | "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" 1134 | }, 1135 | "fabric-contract-api": { 1136 | "version": "2.4.2", 1137 | "resolved": "https://registry.npmjs.org/fabric-contract-api/-/fabric-contract-api-2.4.2.tgz", 1138 | "integrity": "sha512-19lInYwkx9gDXL7BTyk0lfNxO4w4WyqFTIxCiQRTpEQVoolLBDyvtuDpBHDOX4dHMA5YltzGgcxRsBhBAaLTiA==", 1139 | "requires": { 1140 | "class-transformer": "^0.4.0", 1141 | "fabric-shim-api": "2.4.2", 1142 | "fast-safe-stringify": "^2.1.1", 1143 | "get-params": "^0.1.2", 1144 | "reflect-metadata": "^0.1.13", 1145 | "winston": "^3.7.2" 1146 | } 1147 | }, 1148 | "fabric-shim": { 1149 | "version": "2.4.2", 1150 | "resolved": "https://registry.npmjs.org/fabric-shim/-/fabric-shim-2.4.2.tgz", 1151 | "integrity": "sha512-NAl+YMeHVwUiFKgVF8ViKA2o5ZEz89fBZmuedcUE1T13nP31XZ5mFYauyhwu83VyLhh+5LdypkibSZ+aMilQ5g==", 1152 | "requires": { 1153 | "@fidm/x509": "^1.2.1", 1154 | "@grpc/grpc-js": "^1.4.1", 1155 | "@grpc/proto-loader": "^0.6.6", 1156 | "@types/node": "^16.11.1", 1157 | "ajv": "^6.12.2", 1158 | "fabric-contract-api": "2.4.2", 1159 | "fabric-shim-api": "2.4.2", 1160 | "fs-extra": "^10.0.1", 1161 | "reflect-metadata": "^0.1.13", 1162 | "winston": "^3.7.2", 1163 | "yargs": "^17.4.0", 1164 | "yargs-parser": "^21.0.1" 1165 | } 1166 | }, 1167 | "fabric-shim-api": { 1168 | "version": "2.4.2", 1169 | "resolved": "https://registry.npmjs.org/fabric-shim-api/-/fabric-shim-api-2.4.2.tgz", 1170 | "integrity": "sha512-ODjKxDsf6KvN3AhHhztbVSr94MjtTr+Mz/S1SinAbxVPX1yAEJ/ONEoezyu7rLslcr5zJxF1ei2z4Bp08nB2zw==" 1171 | }, 1172 | "fast-deep-equal": { 1173 | "version": "3.1.3", 1174 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", 1175 | "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" 1176 | }, 1177 | "fast-json-stable-stringify": { 1178 | "version": "2.1.0", 1179 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", 1180 | "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" 1181 | }, 1182 | "fast-safe-stringify": { 1183 | "version": "2.1.1", 1184 | "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", 1185 | "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" 1186 | }, 1187 | "fecha": { 1188 | "version": "4.2.3", 1189 | "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", 1190 | "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" 1191 | }, 1192 | "fn.name": { 1193 | "version": "1.1.0", 1194 | "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", 1195 | "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" 1196 | }, 1197 | "fs-extra": { 1198 | "version": "10.1.0", 1199 | "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", 1200 | "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", 1201 | "requires": { 1202 | "graceful-fs": "^4.2.0", 1203 | "jsonfile": "^6.0.1", 1204 | "universalify": "^2.0.0" 1205 | } 1206 | }, 1207 | "get-caller-file": { 1208 | "version": "2.0.5", 1209 | "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", 1210 | "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" 1211 | }, 1212 | "get-params": { 1213 | "version": "0.1.2", 1214 | "resolved": "https://registry.npmjs.org/get-params/-/get-params-0.1.2.tgz", 1215 | "integrity": "sha512-41eOxtlGgHQRbFyA8KTH+w+32Em3cRdfBud7j67ulzmIfmaHX9doq47s0fa4P5o9H64BZX9nrYI6sJvk46Op+Q==" 1216 | }, 1217 | "graceful-fs": { 1218 | "version": "4.2.11", 1219 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", 1220 | "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" 1221 | }, 1222 | "inherits": { 1223 | "version": "2.0.4", 1224 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 1225 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 1226 | }, 1227 | "is-arrayish": { 1228 | "version": "0.3.2", 1229 | "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", 1230 | "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" 1231 | }, 1232 | "is-fullwidth-code-point": { 1233 | "version": "3.0.0", 1234 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 1235 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" 1236 | }, 1237 | "is-stream": { 1238 | "version": "2.0.1", 1239 | "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", 1240 | "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==" 1241 | }, 1242 | "json-schema-traverse": { 1243 | "version": "0.4.1", 1244 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 1245 | "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" 1246 | }, 1247 | "jsonfile": { 1248 | "version": "6.1.0", 1249 | "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", 1250 | "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", 1251 | "requires": { 1252 | "graceful-fs": "^4.1.6", 1253 | "universalify": "^2.0.0" 1254 | } 1255 | }, 1256 | "kuler": { 1257 | "version": "2.0.0", 1258 | "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", 1259 | "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" 1260 | }, 1261 | "lodash.camelcase": { 1262 | "version": "4.3.0", 1263 | "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", 1264 | "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" 1265 | }, 1266 | "logform": { 1267 | "version": "2.5.1", 1268 | "resolved": "https://registry.npmjs.org/logform/-/logform-2.5.1.tgz", 1269 | "integrity": "sha512-9FyqAm9o9NKKfiAKfZoYo9bGXXuwMkxQiQttkT4YjjVtQVIQtK6LmVtlxmCaFswo6N4AfEkHqZTV0taDtPotNg==", 1270 | "requires": { 1271 | "@colors/colors": "1.5.0", 1272 | "@types/triple-beam": "^1.3.2", 1273 | "fecha": "^4.2.0", 1274 | "ms": "^2.1.1", 1275 | "safe-stable-stringify": "^2.3.1", 1276 | "triple-beam": "^1.3.0" 1277 | } 1278 | }, 1279 | "long": { 1280 | "version": "4.0.0", 1281 | "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", 1282 | "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" 1283 | }, 1284 | "ms": { 1285 | "version": "2.1.3", 1286 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 1287 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 1288 | }, 1289 | "one-time": { 1290 | "version": "1.0.0", 1291 | "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", 1292 | "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", 1293 | "requires": { 1294 | "fn.name": "1.x.x" 1295 | } 1296 | }, 1297 | "protobufjs": { 1298 | "version": "6.11.4", 1299 | "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.4.tgz", 1300 | "integrity": "sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==", 1301 | "requires": { 1302 | "@protobufjs/aspromise": "^1.1.2", 1303 | "@protobufjs/base64": "^1.1.2", 1304 | "@protobufjs/codegen": "^2.0.4", 1305 | "@protobufjs/eventemitter": "^1.1.0", 1306 | "@protobufjs/fetch": "^1.1.0", 1307 | "@protobufjs/float": "^1.0.2", 1308 | "@protobufjs/inquire": "^1.1.0", 1309 | "@protobufjs/path": "^1.1.2", 1310 | "@protobufjs/pool": "^1.1.0", 1311 | "@protobufjs/utf8": "^1.1.0", 1312 | "@types/long": "^4.0.1", 1313 | "@types/node": ">=13.7.0", 1314 | "long": "^4.0.0" 1315 | } 1316 | }, 1317 | "punycode": { 1318 | "version": "2.3.0", 1319 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", 1320 | "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==" 1321 | }, 1322 | "readable-stream": { 1323 | "version": "3.6.2", 1324 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", 1325 | "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", 1326 | "requires": { 1327 | "inherits": "^2.0.3", 1328 | "string_decoder": "^1.1.1", 1329 | "util-deprecate": "^1.0.1" 1330 | } 1331 | }, 1332 | "reflect-metadata": { 1333 | "version": "0.1.13", 1334 | "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", 1335 | "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==" 1336 | }, 1337 | "require-directory": { 1338 | "version": "2.1.1", 1339 | "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", 1340 | "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" 1341 | }, 1342 | "safe-buffer": { 1343 | "version": "5.2.1", 1344 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 1345 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" 1346 | }, 1347 | "safe-stable-stringify": { 1348 | "version": "2.4.3", 1349 | "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", 1350 | "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==" 1351 | }, 1352 | "simple-swizzle": { 1353 | "version": "0.2.2", 1354 | "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", 1355 | "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", 1356 | "requires": { 1357 | "is-arrayish": "^0.3.1" 1358 | } 1359 | }, 1360 | "stack-trace": { 1361 | "version": "0.0.10", 1362 | "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", 1363 | "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==" 1364 | }, 1365 | "string_decoder": { 1366 | "version": "1.3.0", 1367 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", 1368 | "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", 1369 | "requires": { 1370 | "safe-buffer": "~5.2.0" 1371 | } 1372 | }, 1373 | "string-width": { 1374 | "version": "4.2.3", 1375 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 1376 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 1377 | "requires": { 1378 | "emoji-regex": "^8.0.0", 1379 | "is-fullwidth-code-point": "^3.0.0", 1380 | "strip-ansi": "^6.0.1" 1381 | } 1382 | }, 1383 | "strip-ansi": { 1384 | "version": "6.0.1", 1385 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 1386 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 1387 | "requires": { 1388 | "ansi-regex": "^5.0.1" 1389 | } 1390 | }, 1391 | "text-hex": { 1392 | "version": "1.0.0", 1393 | "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", 1394 | "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" 1395 | }, 1396 | "triple-beam": { 1397 | "version": "1.3.0", 1398 | "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", 1399 | "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==" 1400 | }, 1401 | "tweetnacl": { 1402 | "version": "1.0.3", 1403 | "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", 1404 | "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==" 1405 | }, 1406 | "universalify": { 1407 | "version": "2.0.0", 1408 | "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", 1409 | "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==" 1410 | }, 1411 | "uri-js": { 1412 | "version": "4.4.1", 1413 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", 1414 | "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", 1415 | "requires": { 1416 | "punycode": "^2.1.0" 1417 | } 1418 | }, 1419 | "util-deprecate": { 1420 | "version": "1.0.2", 1421 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 1422 | "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" 1423 | }, 1424 | "winston": { 1425 | "version": "3.9.0", 1426 | "resolved": "https://registry.npmjs.org/winston/-/winston-3.9.0.tgz", 1427 | "integrity": "sha512-jW51iW/X95BCW6MMtZWr2jKQBP4hV5bIDq9QrIjfDk6Q9QuxvTKEAlpUNAzP+HYHFFCeENhph16s0zEunu4uuQ==", 1428 | "requires": { 1429 | "@colors/colors": "1.5.0", 1430 | "@dabh/diagnostics": "^2.0.2", 1431 | "async": "^3.2.3", 1432 | "is-stream": "^2.0.0", 1433 | "logform": "^2.4.0", 1434 | "one-time": "^1.0.0", 1435 | "readable-stream": "^3.4.0", 1436 | "safe-stable-stringify": "^2.3.1", 1437 | "stack-trace": "0.0.x", 1438 | "triple-beam": "^1.3.0", 1439 | "winston-transport": "^4.5.0" 1440 | } 1441 | }, 1442 | "winston-transport": { 1443 | "version": "4.5.0", 1444 | "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.5.0.tgz", 1445 | "integrity": "sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q==", 1446 | "requires": { 1447 | "logform": "^2.3.2", 1448 | "readable-stream": "^3.6.0", 1449 | "triple-beam": "^1.3.0" 1450 | } 1451 | }, 1452 | "wrap-ansi": { 1453 | "version": "7.0.0", 1454 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", 1455 | "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", 1456 | "requires": { 1457 | "ansi-styles": "^4.0.0", 1458 | "string-width": "^4.1.0", 1459 | "strip-ansi": "^6.0.0" 1460 | } 1461 | }, 1462 | "y18n": { 1463 | "version": "5.0.8", 1464 | "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", 1465 | "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" 1466 | }, 1467 | "yargs": { 1468 | "version": "17.7.2", 1469 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", 1470 | "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", 1471 | "requires": { 1472 | "cliui": "^8.0.1", 1473 | "escalade": "^3.1.1", 1474 | "get-caller-file": "^2.0.5", 1475 | "require-directory": "^2.1.1", 1476 | "string-width": "^4.2.3", 1477 | "y18n": "^5.0.5", 1478 | "yargs-parser": "^21.1.1" 1479 | } 1480 | }, 1481 | "yargs-parser": { 1482 | "version": "21.1.1", 1483 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", 1484 | "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==" 1485 | } 1486 | } 1487 | } 1488 | --------------------------------------------------------------------------------