diff --git a/.github/workflows/tokenization.yml b/.github/workflows/tokenization.yml index 72cfd65a4..f3b7c7b36 100644 --- a/.github/workflows/tokenization.yml +++ b/.github/workflows/tokenization.yml @@ -1,4 +1,5 @@ name: Tokenization + on: pull_request: paths: @@ -19,7 +20,10 @@ jobs: uses: actions/setup-node@v6 with: node-version: '22.x' - - run: (cd Tokenization/backend/central-system; npm i) + + - name: Install backend dependencies + run: (cd Tokenization/backend/central-system; npm i) + lint-check-webapp: name: Check eslint rules for webapp on ubuntu-latest runs-on: ubuntu-latest @@ -30,32 +34,65 @@ jobs: uses: actions/setup-node@v6 with: node-version: '22.x' - - run: (cd Tokenization/webapp; npm i ) - - run: (cd Tokenization/webapp; npm run typecheck) - - run: (cd Tokenization/webapp; npm run lint) + + - name: Install webapp dependencies + run: (cd Tokenization/webapp; npm i) + + - name: Typecheck webapp + run: (cd Tokenization/webapp; npm run typecheck) + + - name: Lint webapp + run: (cd Tokenization/webapp; npm run lint) ui-test: - needs: lint-check-webapp + needs: + - lint-check-webapp name: UI-tests for webapp application runs-on: ubuntu-latest - timeout-minutes: 6 + timeout-minutes: 10 steps: - uses: actions/checkout@v6 + - name: Setup node uses: actions/setup-node@v6 with: node-version: '22.x' - - run: (cd Tokenization/webapp; npm run docker:test) + + - name: Generate test Vault certificates for UI docker:test + run: | + bash Tokenization/scripts/actions-certificates-creation.sh + + - name: Run webapp UI tests (docker) + run: (cd Tokenization/webapp; npm run docker:test) central-system-test: - needs: lint-check-backend + needs: + - lint-check-backend name: Central System backend tests runs-on: ubuntu-latest - timeout-minutes: 6 + timeout-minutes: 20 steps: - uses: actions/checkout@v6 + - name: Setup node uses: actions/setup-node@v6 with: node-version: '22.x' - - run: (cd Tokenization/backend/central-system; npm i; npm run test) + + - name: Generate test Vault certificates + run: | + bash Tokenization/scripts/actions-certificates-creation.sh + + echo "VAULT_ADDR=https://vault.local:9300" >> $GITHUB_ENV + echo "VAULT_AUTH_METHOD=cert" >> $GITHUB_ENV + echo "VAULT_ROLE=central-system" >> $GITHUB_ENV + + echo "VAULT_CACERT_B64=$(base64 -w0 Tokenization/docker/vault/ca.crt)" >> $GITHUB_ENV + echo "VAULT_CENTRAL_SYSTEM_CERT_B64=$(base64 -w0 Tokenization/docker/vault/central-system.crt)" >> $GITHUB_ENV + echo "VAULT_CENTRAL_SYSTEM_KEY_B64=$(base64 -w0 Tokenization/docker/vault/central-system.key)" >> $GITHUB_ENV + + - name: Install backend dependencies + run: (cd Tokenization/backend/central-system; npm i) + + - name: Run all backend tests (unit + Vault integration via docker) + run: (cd Tokenization/backend/central-system; npm run test:all) diff --git a/Tokenization/.gitignore b/Tokenization/.gitignore index 809115f91..d2ef7c2e5 100644 --- a/Tokenization/.gitignore +++ b/Tokenization/.gitignore @@ -6,6 +6,18 @@ backend/wrapper/dist/ backend/wrapper/node_modules/ backend/wrapper/src/run_tests/ backend/node_modules -backend/certs backend/central-system/dist backend/central-system/node_modules +backend/certs +backend/central-system/ca +backend/central-system/vault/vault* +backend/central-system/crt +backend/central-system/policies +central-system.hcl +backend/central-system/.env +database/* +banner.js +.env +*.crt +*.key +*.pem \ No newline at end of file diff --git a/Tokenization/Dockerfile b/Tokenization/Dockerfile index 0f68d6f53..005bd670e 100644 --- a/Tokenization/Dockerfile +++ b/Tokenization/Dockerfile @@ -51,3 +51,9 @@ FROM nginx:1.27 AS reverse-proxy COPY ./docker/provisioning/nginx/conf.d/default.conf /etc/nginx/conf.d EXPOSE 8080 +# ---- Vault ---- +FROM hashicorp/vault:1.20 +COPY ./docker/vault/vault.crt ./docker/vault/vault.key ./docker/vault/ca.crt /vault/config/ +COPY ./docker/vault/central-system-client.crt /vault/config/central-system-client.crt +COPY ./docker/vault/vault-init.sh /usr/local/bin/vault-init.sh +RUN chmod +x /usr/local/bin/vault-init.sh \ No newline at end of file diff --git a/Tokenization/backend/central-system/banner.js b/Tokenization/backend/central-system/banner.js new file mode 100644 index 000000000..e462f6de3 --- /dev/null +++ b/Tokenization/backend/central-system/banner.js @@ -0,0 +1,73 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +import fs from "fs"; +import path from "path"; + +const banner = `/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ +`; + +const processFile = (filePath) => { + try { + const content = fs.readFileSync(filePath, "utf8"); + + if ( + content.includes(`@license`) && + content.includes( + `Copyright 2019-2020 CERN and copyright holders of ALICE O2.` + ) + ) { + return; + } + + const newContent = banner + "\n" + content; + fs.writeFileSync(filePath, newContent, "utf8"); + console.log(`Added banner to: ${filePath}`); + } catch (err) { + console.error(`Error with file ${filePath}:`, err); + } +}; + +const excludedDirs = ["node_modules", "dist"]; +const walkDir = (dir) => { + const files = fs.readdirSync(dir, { withFileTypes: true }); + + for (const file of files) { + const fullPath = path.join(dir, file.name); + + if (file.isDirectory() && !excludedDirs.includes(fullPath)) { + walkDir(fullPath); + } else if (file.isFile()) { + if (/\.(js|ts|jsx|tsx|mjs|cjs|proto)$/.test(file.name)) { + processFile(fullPath); + } + } + } +}; + +const startDir = "./src/"; +walkDir(startDir); +console.log("Banners processed."); \ No newline at end of file diff --git a/Tokenization/backend/central-system/index.js b/Tokenization/backend/central-system/index.js index fa3fe7d1a..47577cc27 100644 --- a/Tokenization/backend/central-system/index.js +++ b/Tokenization/backend/central-system/index.js @@ -29,8 +29,8 @@ http.get( const centralSystemModel = new CentralSystem(4041); http.get( '/tokens/get', - centralSystemModel.tokenController.getTokensHandler.bind( - centralSystemModel.tokenController + centralSystemModel.connectionController.getTokensHandler.bind( + centralSystemModel.connectionController ), { public: true, @@ -39,16 +39,16 @@ http.get( http.post( '/tokens/create', - centralSystemModel.tokenController.createTokenHandler.bind( - centralSystemModel.tokenController + centralSystemModel.connectionController.createTokenHandler.bind( + centralSystemModel.connectionController ), { public: true } ); http.post( '/tokens/revoke', - centralSystemModel.tokenController.revokeTokenHandler.bind( - centralSystemModel.tokenController + centralSystemModel.connectionController.revokeTokenHandler.bind( + centralSystemModel.connectionController ), { public: true } ); @@ -156,11 +156,9 @@ http.get( // Artificially add an error if (tokenId === 3) { - res - .status(500) - .json({ - error: `An error occurred when trying to load logs for token ${tokenId}`, - }); + res.status(500).json({ + error: `An error occurred when trying to load logs for token ${tokenId}`, + }); return; } diff --git a/Tokenization/backend/central-system/package-lock.json b/Tokenization/backend/central-system/package-lock.json index ad8caf93b..a96625368 100644 --- a/Tokenization/backend/central-system/package-lock.json +++ b/Tokenization/backend/central-system/package-lock.json @@ -11,9 +11,13 @@ "@aliceo2/web-ui": "^2.8.4", "@grpc/grpc-js": "^1.13.4", "@grpc/proto-loader": "^0.7.15", - "crypto": "^1.0.1", + "axios": "^1.4.0", "express": "^4.19.2", - "jsonwebtoken": "^9.0.2" + "jose": "^6.1.0", + "jsonwebtoken": "^9.0.2", + "mariadb": "3.0.0", + "sequelize": "6.37.0", + "umzug": "3.8.2" }, "devDependencies": { "@types/express": "^4.17.21", @@ -21,6 +25,9 @@ "@types/jsonwebtoken": "^9.0.9", "@types/node": "^20.4.2", "concurrently": "^9.0.1", + "delayed-stream": "^1.0.0", + "form-data": "^4.0.5", + "formidable": "^3.5.4", "jest": "^30.0.5", "nodemon": "^3.1.10", "supertest": "^7.1.4", @@ -50,6 +57,18 @@ "node": ">= 22.x" } }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -676,9 +695,9 @@ } }, "node_modules/@grpc/grpc-js": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.0.tgz", - "integrity": "sha512-N8Jx6PaYzcTRNzirReJCtADVoq4z7+1KQ4E70jTg/koQiMoUSN1kbNjPOqpPbhMFhfU1/l7ixspPl8dNY+FoUg==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.1.tgz", + "integrity": "sha512-sPxgEWtPUR3EnRJCEtbGZG2iX8LQDUls2wUS3o27jg07KqJFMq6YDeWvMo1wfpmy3rqRdS0rivpLwhqQtEyCuQ==", "license": "Apache-2.0", "dependencies": { "@grpc/proto-loader": "^0.8.0", @@ -1197,6 +1216,41 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@paralleldrive/cuid2": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", @@ -1295,6 +1349,101 @@ "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", "license": "BSD-3-Clause" }, + "node_modules/@rushstack/node-core-library": { + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-5.13.0.tgz", + "integrity": "sha512-IGVhy+JgUacAdCGXKUrRhwHMTzqhWwZUI+qEPcdzsb80heOw0QPbhhoVsoiMF7Klp8eYsp7hzpScMXmOa3Uhfg==", + "license": "MIT", + "dependencies": { + "ajv": "~8.13.0", + "ajv-draft-04": "~1.0.0", + "ajv-formats": "~3.0.1", + "fs-extra": "~11.3.0", + "import-lazy": "~4.0.0", + "jju": "~1.4.0", + "resolve": "~1.22.1", + "semver": "~7.5.4" + }, + "peerDependencies": { + "@types/node": "*" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@rushstack/node-core-library/node_modules/fs-extra": { + "version": "11.3.2", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz", + "integrity": "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@rushstack/node-core-library/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@rushstack/node-core-library/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@rushstack/terminal": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.15.2.tgz", + "integrity": "sha512-7Hmc0ysK5077R/IkLS9hYu0QuNafm+TbZbtYVzCMbeOdMjaRboLKrhryjwZSRJGJzu+TV1ON7qZHeqf58XfLpA==", + "license": "MIT", + "dependencies": { + "@rushstack/node-core-library": "5.13.0", + "supports-color": "~8.1.1" + }, + "peerDependencies": { + "@types/node": "*" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@rushstack/ts-command-line": { + "version": "4.23.7", + "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.23.7.tgz", + "integrity": "sha512-Gr9cB7DGe6uz5vq2wdr89WbVDKz0UeuFEn5H2CfWDe7JvjFFaiV15gi6mqDBTbHhHCWS7w8mF1h3BnIfUndqdA==", + "license": "MIT", + "dependencies": { + "@rushstack/terminal": "0.15.2", + "@types/argparse": "1.0.38", + "argparse": "~1.0.9", + "string-argv": "~0.3.1" + } + }, "node_modules/@sinclair/typebox": { "version": "0.34.41", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", @@ -1371,6 +1520,12 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/argparse": { + "version": "1.0.38", + "resolved": "https://registry.npmjs.org/@types/argparse/-/argparse-1.0.38.tgz", + "integrity": "sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==", + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1437,6 +1592,15 @@ "@types/node": "*" } }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@types/express": { "version": "4.17.25", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", @@ -1463,6 +1627,12 @@ "@types/send": "*" } }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, "node_modules/@types/http-errors": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", @@ -1530,7 +1700,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", - "dev": true, "license": "MIT" }, "node_modules/@types/node": { @@ -1603,6 +1772,12 @@ "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", "license": "MIT" }, + "node_modules/@types/validator": { + "version": "13.15.3", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.3.tgz", + "integrity": "sha512-7bcUmDyS6PN3EuD9SlGGOxM77F8WLVsrwkxyWxKnxzmXoequ6c7741QBrANq6htVRGOITJ7z72mTP6Z4XyuG+Q==", + "license": "MIT" + }, "node_modules/@types/yargs": { "version": "17.0.34", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.34.tgz", @@ -1935,6 +2110,53 @@ "node": ">=0.4.0" } }, + "node_modules/ajv": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.13.0.tgz", + "integrity": "sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.4.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-draft-04": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", + "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", + "license": "MIT", + "peerDependencies": { + "ajv": "^8.5.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -2004,7 +2226,6 @@ "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, "license": "MIT", "dependencies": { "sprintf-js": "~1.0.2" @@ -2033,9 +2254,19 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, "license": "MIT" }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/babel-jest": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz", @@ -2212,7 +2443,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -2453,9 +2683,9 @@ } }, "node_modules/cjs-module-lexer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.1.0.tgz", - "integrity": "sha512-UX0OwmYRYQQetfrLEZeewIFFI+wSTofC+pMBLNuH3RUuu/xzG1oz84UCEDOSoQlN3fZ4+AzmV50ZYvGqkMh9yA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.1.1.tgz", + "integrity": "sha512-+CmxIZ/L2vNcEfvNtLdU0ZQ6mbq3FZnwAP2PPTiKP+1QOoKwlKlPgb8UKV0Dds7QVaMnHm+FwSft2VB0s/SLjQ==", "dev": true, "license": "MIT" }, @@ -2626,13 +2856,45 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "license": "MIT", "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" + "color": "^3.1.3", + "text-hex": "1.0.x" + } + }, + "node_modules/combined-stream/node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, + "node_modules/combined-stream/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/combined-stream/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, + "node_modules/combined-stream/node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" } }, "node_modules/component-emitter": { @@ -2755,13 +3017,6 @@ "node": ">= 8" } }, - "node_modules/crypto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz", - "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==", - "deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in.", - "license": "ISC" - }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -2806,6 +3061,15 @@ "node": ">=0.4.0" } }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -2856,6 +3120,12 @@ "node": ">=0.3.1" } }, + "node_modules/dottie": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.6.tgz", + "integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA==", + "license": "MIT" + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -2893,9 +3163,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.245", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.245.tgz", - "integrity": "sha512-rdmGfW47ZhL/oWEJAY4qxRtdly2B98ooTJ0pdEI4jhVLZ6tNf8fPtov2wS1IRKwFJT92le3x4Knxiwzl7cPPpQ==", + "version": "1.5.248", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.248.tgz", + "integrity": "sha512-zsur2yunphlyAO4gIubdJEXCK6KOVvtpiuDfCIqbM9FjcnMYiyn0ICa3hWfPr0nc41zcLWobgy1iL7VvoOyA2Q==", "dev": true, "license": "ISC" }, @@ -2903,7 +3173,6 @@ "version": "0.13.1", "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -2978,7 +3247,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -3143,6 +3411,28 @@ "url": "https://opencollective.com/express" } }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -3157,6 +3447,15 @@ "dev": true, "license": "MIT" }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/fb-watchman": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", @@ -3177,7 +3476,6 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -3224,6 +3522,26 @@ "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", "license": "MIT" }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -3242,10 +3560,9 @@ } }, "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", - "dev": true, + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -3429,7 +3746,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -3454,7 +3770,6 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, "license": "ISC" }, "node_modules/handlebars": { @@ -3483,7 +3798,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -3505,7 +3819,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -3590,6 +3903,15 @@ "dev": true, "license": "ISC" }, + "node_modules/import-lazy": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", + "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/import-local": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", @@ -3620,6 +3942,15 @@ "node": ">=0.8.19" } }, + "node_modules/inflection": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.4.tgz", + "integrity": "sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==", + "engines": [ + "node >= 0.4.0" + ], + "license": "MIT" + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -3667,11 +3998,25 @@ "node": ">=8" } }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -3700,7 +4045,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -3713,7 +4057,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -4473,10 +4816,16 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/jju": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", + "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==", + "license": "MIT" + }, "node_modules/jose": { - "version": "4.15.9", - "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", - "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.0.tgz", + "integrity": "sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/panva" @@ -4523,6 +4872,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -4536,6 +4891,18 @@ "node": ">=6" } }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/jsonwebtoken": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", @@ -4642,6 +5009,12 @@ "node": ">=8" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, "node_modules/lodash.camelcase": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", @@ -4733,7 +5106,10 @@ "dev": true, "license": "ISC", "dependencies": { - "yallist": "^3.0.2" + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" } }, "node_modules/make-dir": { @@ -4782,17 +5158,53 @@ "tmpl": "1.0.5" } }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", + "node_modules/mariadb": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mariadb/-/mariadb-3.0.0.tgz", + "integrity": "sha512-1uIqD6AWLP5ojMY67XP4+4uRLe9L92HD1ZGU8fidi8cGdYIC+Ghx1JliAtf7lc/tGjOh6J400f/1M4BXVtZFvA==", + "license": "LGPL-2.1-or-later", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@types/geojson": "^7946.0.8", + "@types/node": "^17.0.10", + "denque": "^2.0.1", + "iconv-lite": "^0.6.3", + "moment-timezone": "^0.5.34", + "please-upgrade-node": "^3.2.0" + }, "engines": { - "node": ">= 0.4" + "node": ">= 12" } }, - "node_modules/media-typer": { - "version": "0.3.0", + "node_modules/mariadb/node_modules/@types/node": { + "version": "17.0.45", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz", + "integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==", + "license": "MIT" + }, + "node_modules/mariadb/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", "license": "MIT", @@ -4816,6 +5228,15 @@ "dev": true, "license": "MIT" }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -4829,7 +5250,6 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, "license": "MIT", "dependencies": { "braces": "^3.0.3", @@ -4928,6 +5348,27 @@ "ospec": "ospec/bin/ospec" } }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/moment-timezone": { + "version": "0.5.48", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz", + "integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==", + "license": "MIT", + "dependencies": { + "moment": "^2.29.4" + }, + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -5237,6 +5678,15 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/openid-client/node_modules/jose": { + "version": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/openid-client/node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -5249,12 +5699,6 @@ "node": ">=10" } }, - "node_modules/openid-client/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" - }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -5375,6 +5819,12 @@ "node": ">=8" } }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, "node_modules/path-scurry": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", @@ -5405,6 +5855,12 @@ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "license": "MIT" }, + "node_modules/pg-connection-string": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz", + "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==", + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -5416,7 +5872,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -5448,6 +5903,24 @@ "node": ">=8" } }, + "node_modules/please-upgrade-node": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz", + "integrity": "sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==", + "license": "MIT", + "dependencies": { + "semver-compare": "^1.0.0" + } + }, + "node_modules/pony-cause": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/pony-cause/-/pony-cause-2.1.11.tgz", + "integrity": "sha512-M7LhCsdNbNgiLYiP4WjsfLUuFmCfnjdF6jKe2R9NKl4WFN+HZPGHJZ9lnLP7f9ZnKe3U9nuWD0szirmj+migUg==", + "license": "0BSD", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/pretty-format": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", @@ -5519,6 +5992,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/pstree.remy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", @@ -5526,6 +6005,15 @@ "dev": true, "license": "MIT" }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/pure-rand": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", @@ -5558,6 +6046,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -5632,6 +6140,35 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/resolve-cwd": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", @@ -5655,6 +6192,45 @@ "node": ">=8" } }, + "node_modules/retry-as-promised": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.1.1.tgz", + "integrity": "sha512-hMD7odLOt3LkTjcif8aRZqi/hybjpLNgSk5oF5FCowfCjok6LukpN2bDX7R5wDmbgBQFn7YoBxSagmtXHaJYJw==", + "license": "MIT" + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/rxjs": { "version": "7.8.2", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", @@ -5708,8 +6284,17 @@ "license": "ISC", "bin": { "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "license": "MIT" + }, "node_modules/send": { "version": "0.19.0", "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", @@ -5749,6 +6334,112 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/sequelize": { + "version": "6.37.0", + "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.0.tgz", + "integrity": "sha512-MS6j6aXqWzB3fe9FhmfpQMgVC16bBdYroJCqIqR0l9M2ko8pZdKoi/0PiNWgMyFQDXUHxXyAOG3K07CbnOhteQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/sequelize" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.8", + "@types/validator": "^13.7.17", + "debug": "^4.3.4", + "dottie": "^2.0.6", + "inflection": "^1.13.4", + "lodash": "^4.17.21", + "moment": "^2.29.4", + "moment-timezone": "^0.5.43", + "pg-connection-string": "^2.6.1", + "retry-as-promised": "^7.0.4", + "semver": "^7.5.4", + "sequelize-pool": "^7.1.0", + "toposort-class": "^1.0.1", + "uuid": "^8.3.2", + "validator": "^13.9.0", + "wkx": "^0.5.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependenciesMeta": { + "ibm_db": { + "optional": true + }, + "mariadb": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "oracledb": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-hstore": { + "optional": true + }, + "snowflake-sdk": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "tedious": { + "optional": true + } + } + }, + "node_modules/sequelize-pool": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-7.1.0.tgz", + "integrity": "sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/sequelize/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/sequelize/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/sequelize/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/serve-static": { "version": "1.16.2", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", @@ -5891,6 +6582,21 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/simple-swizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "license": "MIT" + }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -5952,7 +6658,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true, "license": "BSD-3-Clause" }, "node_modules/sqlstring": { @@ -6010,6 +6715,15 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "license": "MIT" }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -6202,7 +6916,7 @@ "qs": "^6.11.2" }, "engines": { - "node": ">=14.18.0" + "node": ">=4" } }, "node_modules/superagent/node_modules/debug": { @@ -6261,7 +6975,6 @@ "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -6273,6 +6986,18 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/synckit": { "version": "0.11.11", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", @@ -6367,7 +7092,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -6385,6 +7109,12 @@ "node": ">=0.6" } }, + "node_modules/toposort-class": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz", + "integrity": "sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg==", + "license": "MIT" + }, "node_modules/touch": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", @@ -6610,6 +7340,34 @@ "node": ">=0.8.0" } }, + "node_modules/umzug": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/umzug/-/umzug-3.8.2.tgz", + "integrity": "sha512-BEWEF8OJjTYVC56GjELeHl/1XjFejrD7aHzn+HldRJTx+pL1siBrKHZC8n4K/xL3bEzVA9o++qD1tK2CpZu4KA==", + "license": "MIT", + "dependencies": { + "@rushstack/ts-command-line": "^4.12.2", + "emittery": "^0.13.0", + "fast-glob": "^3.3.2", + "pony-cause": "^2.1.4", + "type-fest": "^4.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/umzug/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -6623,6 +7381,15 @@ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -6698,6 +7465,15 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -6713,6 +7489,15 @@ "node": ">= 0.4.0" } }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -6735,6 +7520,15 @@ "node": ">=10.12.0" } }, + "node_modules/validator": { + "version": "13.15.23", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.23.tgz", + "integrity": "sha512-4yoz1kEWqUjzi5zsPbAS/903QXSYp0UOtHsPpp7p9rHAw/W+dkInskAE386Fat3oKRROwO98d9ZB0G4cObgUyw==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -6834,6 +7628,15 @@ "node": ">= 6" } }, + "node_modules/wkx": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.5.0.tgz", + "integrity": "sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", @@ -6988,10 +7791,9 @@ } }, "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "license": "ISC" }, "node_modules/yargs": { diff --git a/Tokenization/backend/central-system/package.json b/Tokenization/backend/central-system/package.json index 95739597b..835f28f69 100644 --- a/Tokenization/backend/central-system/package.json +++ b/Tokenization/backend/central-system/package.json @@ -10,17 +10,28 @@ "watch:node": "nodemon --watch dist --ext js,json --exec \"node index.js\"", "dev": "npm run build && concurrently -k \"npm:watch:*\"", "start": "node index.js", - "test": "jest --runInBand", - "docker:up": "cd ../.. && docker compose -f docker-compose.yml up", - "docker:down": "cd ../.. && docker compose -f docker-compose.yml down" + "docker:build": "cd ../.. && docker compose -f docker-compose.dev.yml -f docker-compose.yml build", + "docker:up": "cd ../.. && docker compose -f docker-compose.dev.yml -f docker-compose.yml up", + "docker:up:build": "npm run docker:build && npm run docker:up", + "docker:down": "cd ../.. && docker compose -f docker-compose.dev.yml -f docker-compose.yml down", + "test": "jest --runInBand --testPathIgnorePatterns=VaultIntegration.spec.ts", + "test:unit": "npm test", + "test:vault:integration": "jest --runInBand tests/VaultTests/VaultIntegration.spec.ts", + "docker:test:vault": "cd ../.. && docker compose -p tokenization-tests -f docker-compose.dev.yml up --build --abort-on-container-exit central-tests && docker compose -p tokenization-tests -f docker-compose.dev.yml down -v", + "test:vault": "npm run docker:test:vault", + "test:all": "npm run test:unit && npm run test:vault" }, "dependencies": { "@aliceo2/web-ui": "^2.8.4", - "crypto": "^1.0.1", + "@grpc/grpc-js": "^1.13.4", + "@grpc/proto-loader": "^0.7.15", "express": "^4.19.2", + "jose": "^6.1.0", "jsonwebtoken": "^9.0.2", - "@grpc/grpc-js": "^1.13.4", - "@grpc/proto-loader": "^0.7.15" + "mariadb": "3.0.0", + "axios": "^1.4.0", + "sequelize": "6.37.0", + "umzug": "3.8.2" }, "devDependencies": { "@types/express": "^4.17.21", @@ -28,6 +39,9 @@ "@types/jsonwebtoken": "^9.0.9", "@types/node": "^20.4.2", "concurrently": "^9.0.1", + "delayed-stream": "^1.0.0", + "form-data": "^4.0.5", + "formidable": "^3.5.4", "jest": "^30.0.5", "nodemon": "^3.1.10", "supertest": "^7.1.4", diff --git a/Tokenization/backend/central-system/src/controllers/TokensController.ts b/Tokenization/backend/central-system/src/controllers/ConnectionController.ts similarity index 98% rename from Tokenization/backend/central-system/src/controllers/TokensController.ts rename to Tokenization/backend/central-system/src/controllers/ConnectionController.ts index 9ba37571d..87d8c70be 100644 --- a/Tokenization/backend/central-system/src/controllers/TokensController.ts +++ b/Tokenization/backend/central-system/src/controllers/ConnectionController.ts @@ -12,7 +12,7 @@ * or submit itself to any jurisdiction. */ -import { CentralSystemWrapper } from '../wrapper/CentralSystemWrapper'; +import { CentralSystemWrapper } from '../lib/CentralSystemWrapper'; import { DuplexMessageEvent, ConnectionDirection, @@ -31,7 +31,7 @@ import { TokensGetService } from '../services/TokensGetService.js'; /** * @description Controller for managing tokens in the Central System. */ -export class TokensController { +export class ConnectionController { private _logger; /** diff --git a/Tokenization/backend/central-system/src/controllers/VaultController.ts b/Tokenization/backend/central-system/src/controllers/VaultController.ts new file mode 100644 index 000000000..46f45077c --- /dev/null +++ b/Tokenization/backend/central-system/src/controllers/VaultController.ts @@ -0,0 +1,224 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +import { VaultCredentialsService } from '../services/VaultCredentialsService.js'; +import { VaultAuthService } from '../services/VaultAuthService.js'; +import { VaultSignService } from '../services/VaultSignService.js'; + +import { Agent } from 'https'; + +import { + SignTokenReq, + GetCredentialReq, + CreateOrUpdateCredentialReq, +} from '../lib/utils/event-req-types.js'; +import { registerBusHandler } from '../lib/event-bus/register-bus-handler.js'; +import { EventType } from '../lib/utils/events.js'; + +import { LogManager } from '@aliceo2/web-ui'; + +import { + SignPayload, + VaultReadResponse, + VaultKvWritePayload, +} from '../types/vault_types.js'; + +/** + * @description Controller for managing interactions with the Vault service. + */ +export class VaultController { + // Agent for HTTPS requests + private readonly _agent: Agent; + // Access token for Vault + private _vaultAccessToken: string = ''; + private _logger; + /** + * @description Constructs a new VaultController. Initializes the HTTPS agent using + * TLS certificates provided via environment variables. + * @param tokenSignService - Service for signing tokens. + * @param authService - Service for authenticating with the vault. + * @param credentialsService - Service for retrieving credentials from the vault. + * @throws Will throw an error if required environment variables are missing. + */ + constructor( + private readonly _tokenSignService: VaultSignService, + private readonly _authService: VaultAuthService, + private readonly _credentialsService: VaultCredentialsService + ) { + this._logger = LogManager.getLogger('VaultController'); + const caPem = process.env.VAULT_CACERT_B64; + const certPem = process.env.VAULT_CENTRAL_SYSTEM_CERT_B64; + const keyPem = process.env.VAULT_CENTRAL_SYSTEM_KEY_B64; + if (!caPem || !certPem || !keyPem) { + throw new Error( + 'Missing required environment variables for TLS certificates.' + ); + } + + this._agent = new Agent({ + keepAlive: true, + ca: Buffer.from(caPem, 'base64'), + cert: Buffer.from(certPem, 'base64'), + key: Buffer.from(keyPem, 'base64'), + }); + } + + /** + * @description Signs a token using the VaultSignService. + * @param payload - The payload containing the subject and optional claims for the token. + * @returns A promise that resolves to the signed token and its expiration time. + * @throws Will throw an error if signing fails. + */ + public async signToken(payload: SignPayload): Promise { + try { + return await this._tokenSignService.signToken( + process.env.VAULT_ADDR! + '/v1/transit/sign/tokenization-signing', + this._vaultAccessToken, + this._agent, + JSON.stringify(payload.data) + ); + } catch (error: any) { + this._logger.errorMessage( + `Error signing token with Vault: ${error.message}` + ); + throw error; + } + } + /** + * @description Logs into the vault using the VaultAuthService and retrieves an access token. + * @returns A promise that resolves to the access token. + * @throws Will throw an error if login fails. + */ + public async loginVault(): Promise { + try { + this._vaultAccessToken = await this._authService.login( + process.env.VAULT_ADDR! + + `/v1/auth/${process.env.VAULT_AUTH_METHOD}/login`, + this._agent, + JSON.stringify({ + name: process.env.VAULT_ROLE, + }) + ); + } catch (error: any) { + this._logger.errorMessage(`Vault login failed: ${error.message}`); + throw error; + } + this._logger.info( + `Logged into Vault successfully.Token: ${this._vaultAccessToken.slice( + 0, + 6 + )}...` + ); + } + + /** + * @description Renews the vault access token using the VaultAuthService. + * @returns A promise that resolves to the renewed access token. + */ + public async renewVaultToken(): Promise { + try { + await this._authService.renew( + process.env.VAULT_ADDR! + '/v1/auth/token/renew-self', + this._vaultAccessToken, + this._agent, + null + ); + } catch (error: any) { + this._logger.errorMessage(`Vault token renewal failed: ${error.message}`); + this._logger.info('Attempting to re-login to Vault...'); + await this.loginVault(); + } + this._logger.info('Vault token renewed successfully.'); + } + + /** + * @description Retrieves a credential from the vault using the VaultCredentialsService. + * @param path - The path of the credential to retrieve. + * @returns A promise that resolves to the retrieved credential. + * @throws Will throw an error if retrieval fails. + */ + public async getCredentialFromVault( + path: string + ): Promise { + try { + return await this._credentialsService.getCredential( + process.env.VAULT_ADDR! + `/v1/tokenization/data/${path}`, + this._vaultAccessToken, + this._agent + ); + } catch (error: any) { + this._logger.errorMessage( + `Error getting credential from Vault: ${error.message}` + ); + throw error; + } + } + + /** + * @description Creates or updates a credential in the vault using the VaultCredentialsService. + * @param path - The path where the credential should be stored. + * @param body - The body of the credential to create or update. + * @returns A promise that resolves when the operation is complete. + * @throws Will throw an error if the operation fails. + */ + public async createOrUpdateCredentialInVault( + path: string, + body: VaultKvWritePayload + ): Promise { + try { + await this._credentialsService.createOrUpdateCredential( + process.env.VAULT_ADDR! + `/v1/tokenization/data/${path}`, + this._vaultAccessToken, + this._agent, + JSON.stringify(body) + ); + } catch (error: any) { + this._logger.errorMessage( + `Error creating/updating credential in Vault: ${error.message}` + ); + throw error; + } + } + + /** + * @description Registers the event handlers for vault-related operations. + * This method sets up handlers for signing tokens, logging in, renewing tokens, + * and managing credentials in the vault. + */ + public register() { + registerBusHandler( + EventType.SIGN_TOKEN_VAULT, + async (payload) => this.signToken(payload) + ); + + registerBusHandler(EventType.LOGIN_VAULT, async () => + this.loginVault() + ); + + registerBusHandler(EventType.RENEW_VAULT_TOKEN, async () => + this.renewVaultToken() + ); + + registerBusHandler( + EventType.GET_CREDENTIAL_VAULT, + async (payload) => this.getCredentialFromVault(payload.path) + ); + + registerBusHandler( + EventType.CREATE_OR_UPDATE_CREDENTIAL_VAULT, + async (payload) => + this.createOrUpdateCredentialInVault(payload.path, payload.body) + ); + } +} diff --git a/Tokenization/backend/central-system/src/wrapper/CentralSystemWrapper.ts b/Tokenization/backend/central-system/src/lib/CentralSystemWrapper.ts similarity index 98% rename from Tokenization/backend/central-system/src/wrapper/CentralSystemWrapper.ts rename to Tokenization/backend/central-system/src/lib/CentralSystemWrapper.ts index f1f8893fa..97b721589 100644 --- a/Tokenization/backend/central-system/src/wrapper/CentralSystemWrapper.ts +++ b/Tokenization/backend/central-system/src/lib/CentralSystemWrapper.ts @@ -163,7 +163,7 @@ export class CentralSystemWrapper { } /** - * @desciprion Starts the gRPC server and binds it to the specified in class port. + * @description Starts the gRPC server and binds it to the specified in class port. */ public listen() { const addr = `localhost:${this.port}`; diff --git a/Tokenization/backend/central-system/src/lib/database/Database.ts b/Tokenization/backend/central-system/src/lib/database/Database.ts new file mode 100644 index 000000000..7f3be44f9 --- /dev/null +++ b/Tokenization/backend/central-system/src/lib/database/Database.ts @@ -0,0 +1,49 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +import { SequelizeDatabase } from './SequelizeDatabase.js'; +import { LogManager } from '@aliceo2/web-ui'; + +// Database class to create and manage the database connection +class Database { + /** + * Creates and initializes the database connection. + * @param config - Database configuration object. + * @returns Initialized SequelizeDatabase instance. + */ + public static async createDatabase( + config: object + ): Promise { + const database = new SequelizeDatabase(config); + + await database.connect(); + await database.migrate(); + + return database; + } +} + +const logger = LogManager.getLogger('Database'); + +export const db = await Database.createDatabase({ + host: process.env.DB_HOST ?? 'database', + port: Number(process.env.DB_PORT ?? 3306), + username: process.env.DB_USER ?? 'central-system', + password: process.env.DB_PASSWORD ?? 'cern', + database: process.env.DB_NAME ?? 'tokenization', + charset: 'utf8mb4', + collate: 'utf8mb4_unicode_ci', + timezone: process.env.DB_TZ ?? '+00:00', + logging: process.env.DB_LOGGING ?? false, +}); diff --git a/Tokenization/backend/central-system/src/lib/database/SequelizeDatabase.ts b/Tokenization/backend/central-system/src/lib/database/SequelizeDatabase.ts new file mode 100644 index 000000000..d14ecb2cd --- /dev/null +++ b/Tokenization/backend/central-system/src/lib/database/SequelizeDatabase.ts @@ -0,0 +1,118 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +import { LogManager } from '@aliceo2/web-ui'; +import { Sequelize } from 'sequelize'; +import { getConfig } from './utils/getConfig.js'; +import { models } from './models/models.js'; +import { dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { join } from 'path'; +import { createUmzug } from './umzug.js'; +import { SequelizeDatabaseConfig } from './utils/sequelizeDatabaseConfig'; +import { SequelizeStorage } from 'umzug'; + +export class SequelizeDatabase { + private _logger; + public sequelize: Sequelize; + private _models: object; + private _dbConfig: SequelizeDatabaseConfig; + + /** + * Initializes the Sequelize database connection. + * @param config - Database configuration object. + */ + constructor(config: object) { + this._logger = LogManager.getLogger('database/sequelize'); + + if (!config) { + this._logger.warnMessage('No database configuration provided'); + } + this._dbConfig = getConfig(config); + + const { + host, + port, + username, + password, + database, + charset, + collate, + timezone, + logging, + } = this._dbConfig; + + this.sequelize = new Sequelize(database, username, password, { + host, + port, + dialect: 'mariadb', + dialectOptions: { + charset, + collate, + timezone, + }, + logging, + define: { + underscored: true, + }, + }); + this._models = models(this.sequelize); + this._logger.infoMessage('Database connection initialized successfully.'); + } + + /** Connects to the database with retry logic. */ + async connect() { + const { retryThrottle } = this._dbConfig; + let connected = false; + + while (!connected) { + try { + await this.sequelize.authenticate(); + connected = true; + this._logger.infoMessage(`Successfully connected to database + '${this._dbConfig.database}' on '${this._dbConfig.host}:${this._dbConfig.port}'`); + } catch (error) { + this._logger.errorMessage(`Unable to connect to db: ${error}`); + this._logger.debugMessage(`Retrying in ${retryThrottle} ms...`); + await new Promise((resolve) => setTimeout(resolve, retryThrottle)); + } + } + } + + /** Executes pending database migrations. */ + async migrate() { + this._logger.debugMessage('Executing pending migrations...'); + try { + const __filename = fileURLToPath(import.meta.url); + const __dirname = dirname(__filename); + const umzug = createUmzug( + this.sequelize, + join(__dirname, 'migrations'), + new SequelizeStorage({ + sequelize: this.sequelize, + }) + ); + await umzug.up(); + this._logger.infoMessage('Migrations completed successfully.'); + } catch (error) { + this._logger.errorMessage(`Error executing migrations: ${error}`); + throw error; + } + } + + /** Access to the database models. */ + get models() { + return this._models; + } +} diff --git a/Tokenization/backend/central-system/src/lib/database/migrations/20251108-001-create-tokens.mts b/Tokenization/backend/central-system/src/lib/database/migrations/20251108-001-create-tokens.mts new file mode 100644 index 000000000..5703ba744 --- /dev/null +++ b/Tokenization/backend/central-system/src/lib/database/migrations/20251108-001-create-tokens.mts @@ -0,0 +1,62 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +import type { QueryInterface } from 'sequelize'; + +/** Umzug migration: create `tokens` table */ +export async function up( + q: QueryInterface, + Sequelize: typeof import('sequelize') +) { + await q.createTable('tokens', { + id: { + type: Sequelize.INTEGER, + allowNull: false, + autoIncrement: true, + primaryKey: true, + }, + audience: { type: Sequelize.STRING(255), allowNull: false }, + subject: { type: Sequelize.STRING(255), allowNull: false }, + token_object: { type: Sequelize.JSON, allowNull: false }, + created_at: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'), + }, + updated_at: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.literal( + 'CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP' + ), + }, + }); + + await q.addIndex('tokens', ['audience'], { name: 'tokens_audience_idx' }); + await q.addIndex('tokens', ['subject'], { name: 'tokens_subject_idx' }); + await q.addIndex('tokens', ['created_at'], { name: 'tokens_created_at_idx' }); +} + +export async function down(q: QueryInterface) { + try { + await q.removeIndex('tokens', 'tokens_created_at_idx'); + } catch {} + try { + await q.removeIndex('tokens', 'tokens_subject_idx'); + } catch {} + try { + await q.removeIndex('tokens', 'tokens_audience_idx'); + } catch {} + await q.dropTable('tokens'); +} diff --git a/Tokenization/backend/central-system/src/lib/database/migrations/20251108-002-create-archive-tokens.mts b/Tokenization/backend/central-system/src/lib/database/migrations/20251108-002-create-archive-tokens.mts new file mode 100644 index 000000000..99b9f8926 --- /dev/null +++ b/Tokenization/backend/central-system/src/lib/database/migrations/20251108-002-create-archive-tokens.mts @@ -0,0 +1,68 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +import type { QueryInterface } from 'sequelize'; + +/** Umzug migration: create `archive-tokens` table */ +export async function up( + q: QueryInterface, + Sequelize: typeof import('sequelize') +) { + await q.createTable('archive-tokens', { + id: { + type: Sequelize.INTEGER, + allowNull: false, + autoIncrement: true, + primaryKey: true, + }, + audience: { type: Sequelize.STRING(255), allowNull: false }, + subject: { type: Sequelize.STRING(255), allowNull: false }, + token_object: { type: Sequelize.JSON, allowNull: false }, + created_at: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'), + }, + updated_at: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.literal( + 'CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP' + ), + }, + }); + + await q.addIndex('archive-tokens', ['audience'], { + name: 'archive_tokens_audience_idx', + }); + await q.addIndex('archive-tokens', ['subject'], { + name: 'archive_tokens_subject_idx', + }); + await q.addIndex('archive-tokens', ['created_at'], { + name: 'archive_tokens_created_at_idx', + }); +} + +export async function down(q: QueryInterface) { + try { + await q.removeIndex('archive-tokens', 'archive_tokens_created_at_idx'); + } catch {} + try { + await q.removeIndex('archive-tokens', 'archive_tokens_subject_idx'); + } catch {} + try { + await q.removeIndex('archive-tokens', 'archive_tokens_audience_idx'); + } catch {} + await q.dropTable('archive-tokens'); +} diff --git a/Tokenization/backend/central-system/src/lib/database/models/ArchiveTokenModel.ts b/Tokenization/backend/central-system/src/lib/database/models/ArchiveTokenModel.ts new file mode 100644 index 000000000..ae514a699 --- /dev/null +++ b/Tokenization/backend/central-system/src/lib/database/models/ArchiveTokenModel.ts @@ -0,0 +1,77 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +import { Sequelize, Model, DataTypes } from 'sequelize'; + +// Define the structure of the token object +interface ArchivedTokenAttributes { + sub: string; + aud: string; + iss: string; + method: string; + jti: string; +} + +// Define the Token model +export class ArchiveToken extends Model { + declare id: number; + declare audience: string; + declare subject: string; + declare created_at: Date; + declare updated_at: Date; + declare token_object: ArchivedTokenAttributes; +} + +/* Initialize and export the Token model */ +export default (sequelize: Sequelize): typeof ArchiveToken => + ArchiveToken.init( + { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + allowNull: false, + autoIncrement: true, + }, + audience: { + type: DataTypes.STRING(255), + allowNull: false, + }, + subject: { + type: DataTypes.STRING(255), + allowNull: false, + }, + created_at: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, + }, + updated_at: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, + }, + token_object: { + type: DataTypes.JSON, + allowNull: false, + }, + }, + { + sequelize, + tableName: 'archive-tokens', + timestamps: true, + underscored: true, + createdAt: 'created_at', + updatedAt: 'updated_at', + } + ); diff --git a/Tokenization/backend/central-system/src/lib/database/models/TokenModel.ts b/Tokenization/backend/central-system/src/lib/database/models/TokenModel.ts new file mode 100644 index 000000000..f4a51b471 --- /dev/null +++ b/Tokenization/backend/central-system/src/lib/database/models/TokenModel.ts @@ -0,0 +1,78 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +import { Sequelize, Model, DataTypes } from 'sequelize'; + +// Define the structure of the token object +interface TokenAttributes { + sub: string; + aud: string; + iss: string; + iat: Record; + exp: Record; + jti: string; +} + +// Define the Token model +export class Token extends Model { + declare id: number; + declare audience: string; + declare subject: string; + declare created_at: Date; + declare updated_at: Date; + declare token_object: TokenAttributes; +} + +/* Initialize and export the Token model */ +export default (sequelize: Sequelize): typeof Token => + Token.init( + { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + allowNull: false, + autoIncrement: true, + }, + audience: { + type: DataTypes.STRING(255), + allowNull: false, + }, + subject: { + type: DataTypes.STRING(255), + allowNull: false, + }, + created_at: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, + }, + updated_at: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, + }, + token_object: { + type: DataTypes.JSON, + allowNull: false, + }, + }, + { + sequelize, + tableName: 'tokens', + timestamps: true, + underscored: true, + createdAt: 'created_at', + updatedAt: 'updated_at', + } + ); diff --git a/Tokenization/backend/central-system/src/lib/database/models/models.ts b/Tokenization/backend/central-system/src/lib/database/models/models.ts new file mode 100644 index 000000000..1c5d20c76 --- /dev/null +++ b/Tokenization/backend/central-system/src/lib/database/models/models.ts @@ -0,0 +1,29 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +// Import all models here and export them +import TokenModel from './TokenModel.js'; +import ArchiveTokenModel from './ArchiveTokenModel.js'; +import { Sequelize } from 'sequelize'; + +export function models(sequelize: Sequelize): { + Token: ReturnType; + ArchiveToken: ReturnType; +} { + const models = { + Token: TokenModel(sequelize), + ArchiveToken: ArchiveTokenModel(sequelize), + }; + return models; +} diff --git a/Tokenization/backend/central-system/src/lib/database/umzug.ts b/Tokenization/backend/central-system/src/lib/database/umzug.ts new file mode 100644 index 000000000..93ee822fa --- /dev/null +++ b/Tokenization/backend/central-system/src/lib/database/umzug.ts @@ -0,0 +1,67 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +import pkg, { SequelizeStorage } from 'umzug'; +import { Sequelize } from 'sequelize'; +import { LogManager } from '@aliceo2/web-ui'; +import { MigrationParams } from 'umzug'; + +const { Umzug } = pkg; +const logger = LogManager.getLogger('database/umzug'); + +export const createUmzug = ( + sequelize: Sequelize, + migrationsDirectory: string, + storage: SequelizeStorage +) => + new Umzug({ + migrations: { + glob: `${migrationsDirectory}/*.mjs`, + resolve: ({ + name, + path: migrationPath, + context, + }: MigrationParams) => { + const loadMigration = async () => { + if (!migrationPath) { + throw new Error(`Missing migration path for '${name}'`); + } + const migration = await import(migrationPath); + if ( + typeof migration.up !== 'function' || + typeof migration.down !== 'function' + ) { + throw new Error( + `Migration '${name}' is missing valid up/down functions.` + ); + } + return migration; + }; + + return { + name, + up: async () => + (await loadMigration()).up(context.getQueryInterface(), Sequelize), + down: async () => + (await loadMigration()).down( + context.getQueryInterface(), + Sequelize + ), + }; + }, + }, + context: sequelize, + storage, + logger: logger, + }); diff --git a/Tokenization/backend/central-system/src/lib/database/utils/expireToken.ts b/Tokenization/backend/central-system/src/lib/database/utils/expireToken.ts new file mode 100644 index 000000000..96a6cc77a --- /dev/null +++ b/Tokenization/backend/central-system/src/lib/database/utils/expireToken.ts @@ -0,0 +1,70 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +import { LogManager } from '@aliceo2/web-ui'; +import { ArchiveToken } from './../models/ArchiveTokenModel.js'; +import { Token } from './../models/TokenModel.js'; +import { Sequelize } from 'sequelize'; + +type Method = 'GET' | 'POST' | 'PUT' | 'DELETE' | string; + +const logger = LogManager.getLogger('database/utils/expireToken'); + +/** + * Expires a token by archiving it and removing the specified method. + * If it's the last method, the token is deleted entirely. + * @param sequelize - Sequelize instance for database operations. + * @param id - ID of the token to expire. + * @param method - HTTP method to expire from the token. + */ +export default async ( + sequelize: Sequelize, + id: number, + method: Method +): Promise => { + const token: Token | null = await Token.findByPk(id); + if (!token) { + logger.info('No such token in database.'); + return; + } + + const { sub, aud, iss, iat, jti } = token.token_object; + const methods: string[] = Object.keys(iat); + + await sequelize.transaction(async (tx) => { + await ArchiveToken.create( + { + audience: token.audience, + subject: token.subject, + token_object: { + sub, + aud, + iss, + method, + jti, + }, + }, + { transaction: tx } + ); + + if (methods.length === 1) { + await token.destroy({ transaction: tx }); + return; + } + + const { [method]: _removed, ...newIat } = iat; + token.token_object = { ...token.token_object, iat: newIat }; + await token.save({ transaction: tx }); + }); +}; diff --git a/Tokenization/backend/central-system/src/lib/database/utils/getConfig.ts b/Tokenization/backend/central-system/src/lib/database/utils/getConfig.ts new file mode 100644 index 000000000..5271d526b --- /dev/null +++ b/Tokenization/backend/central-system/src/lib/database/utils/getConfig.ts @@ -0,0 +1,37 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +import { SequelizeDatabaseConfig } from './sequelizeDatabaseConfig.js'; +/** + * Returns database configuration with default values if not provided. + * @param config - Partial database configuration object. + * @returns Complete database configuration object. + */ +export function getConfig( + config?: Partial +): SequelizeDatabaseConfig { + const c = config ?? {}; + return { + host: c.host ?? 'localhost', + port: c.port ?? 3306, + username: c.username ?? 'cern', + password: c.password ?? 'cern', + database: c.database ?? 'tkn', + charset: c.charset ?? 'utf8mb4', + collate: c.collate ?? 'utf8mb4_general_ci', + timezone: c.timezone ?? '+00:00', + logging: c.logging ?? false, + retryThrottle: c.retryThrottle ?? 5000, + }; +} diff --git a/Tokenization/backend/central-system/src/lib/database/utils/sequelizeDatabaseConfig.d.ts b/Tokenization/backend/central-system/src/lib/database/utils/sequelizeDatabaseConfig.d.ts new file mode 100644 index 000000000..33a9329e5 --- /dev/null +++ b/Tokenization/backend/central-system/src/lib/database/utils/sequelizeDatabaseConfig.d.ts @@ -0,0 +1,26 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +export interface SequelizeDatabaseConfig { + host: string; + port: number; + username: string; + password: string; + database: string; + charset: string; + collate: string; + timezone: string; + logging: boolean | ((sql: string, timing?: number) => void); + retryThrottle: number; +} diff --git a/Tokenization/backend/central-system/src/lib/event-bus/event-bus.ts b/Tokenization/backend/central-system/src/lib/event-bus/event-bus.ts new file mode 100644 index 000000000..e498e8467 --- /dev/null +++ b/Tokenization/backend/central-system/src/lib/event-bus/event-bus.ts @@ -0,0 +1,17 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +import { EventEmitter } from "node:events"; +export const bus = new EventEmitter(); +bus.setMaxListeners(100); diff --git a/Tokenization/backend/central-system/src/lib/event-bus/register-bus-handler.ts b/Tokenization/backend/central-system/src/lib/event-bus/register-bus-handler.ts new file mode 100644 index 000000000..f4615de89 --- /dev/null +++ b/Tokenization/backend/central-system/src/lib/event-bus/register-bus-handler.ts @@ -0,0 +1,48 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +import {bus} from "./event-bus.js"; + +// Generic handler type +type Handler = (payload: TPayload) => Promise; + +/** + * @description Registers a handler for a specific event on the event bus. + * The handler processes incoming requests and sends back responses or errors. + * @param event - The name of the event to listen for. + * @param handler - The async function that processes the event payload and returns a result. + */ + +function registerBusHandler( + event: string, + handler: Handler +) { + bus.on(event, async ({ id, replyEvent, payload }: { id: string; replyEvent: string; payload: TPayload }) => { + try { + const data = await handler(payload); + bus.emit(replyEvent, { ok: true as const, data }); + } catch (err: any) { + bus.emit(replyEvent, { + ok: false as const, + error: { + message: err?.message ?? "Unknown error", + code: err?.code, + stack: err?.stack, + }, + }); + } + }); +} + +export { registerBusHandler }; \ No newline at end of file diff --git a/Tokenization/backend/central-system/src/lib/event-bus/rpc.ts b/Tokenization/backend/central-system/src/lib/event-bus/rpc.ts new file mode 100644 index 000000000..c56c0a32d --- /dev/null +++ b/Tokenization/backend/central-system/src/lib/event-bus/rpc.ts @@ -0,0 +1,75 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +import { bus } from './event-bus.js'; +import { randomUUID } from 'node:crypto'; + +// Error structure for RPC failures +export type RpcError = { message: string; code?: string; stack?: string }; + +// Custom Error class to include code and stack +export class RpcCustomError extends Error { + code?: string; + override stack?: string; + constructor(message: string, code?: string, stack?: string) { + super(message); + this.name = 'RpcCustomError'; + this.code = code; + if (stack) this.stack = stack; + } +} + +// Payload structure for emitted events +interface RpcEventPayload { + id: string; + replyEvent: string; + payload: Req; +} + +/** + * @description Emits an event on the bus and waits for a corresponding reply event. + * The function generates a unique ID for the request, listens for the reply, + * and resolves or rejects based on the response received. + * @param baseEvent - The base name of the event to emit. + * @param payload - The payload to send with the event. + * @param timeoutMs - Optional timeout in milliseconds to wait for a reply (default is 10 seconds). + * @returns A promise that resolves with the response data or rejects with an error. + * @throws Will throw an error if the timeout is reached or if the response indicates a failure. + */ +export async function emitAndWait( + baseEvent: string, + payload: Req, + { timeoutMs = 10_000 }: { timeoutMs?: number } = {} +): Promise { + const id = randomUUID(); + const replyEvent = `${baseEvent}:REPLY:${id}`; + + return new Promise((resolve, reject) => { + const onReply = (msg: { ok: true; data: Res } | { ok: false; error: RpcError }) => { + clearTimeout(timer); + bus.off(replyEvent, onReply); + if (msg.ok) resolve(msg.data); + else reject(new RpcCustomError(msg.error.message, msg.error.code, msg.error.stack)); + }; + + const timer = setTimeout(() => { + bus.off(replyEvent, onReply); + reject(new Error(`RPC timeout waiting for ${replyEvent}`)); + }, timeoutMs); + + bus.on(replyEvent, onReply); + const eventPayload: RpcEventPayload = { id, replyEvent, payload }; + bus.emit(baseEvent, eventPayload); + }); +} diff --git a/Tokenization/backend/central-system/src/lib/utils/Scheduler.ts b/Tokenization/backend/central-system/src/lib/utils/Scheduler.ts new file mode 100644 index 000000000..39f201a4c --- /dev/null +++ b/Tokenization/backend/central-system/src/lib/utils/Scheduler.ts @@ -0,0 +1,70 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +import { LogManager } from '@aliceo2/web-ui'; + +const logger = LogManager.getLogger('utils/Scheduler'); + +type Key = string; + +export type ScheduledJobs = Map; + +export class ArchiveScheduler { + private jobs: ScheduledJobs = new Map(); + + private key(tokenId: number, method: string): Key { + return `${tokenId}:${method}`; + } + + /** Schedule a job to run after delayMs milliseconds. */ + schedule( + tokenId: number, + method: string, + delayMs: number, + run: () => Promise + ): () => void { + const k = this.key(tokenId, method); + + // prevent duplicates + this.cancel(tokenId, method); + + const t = setTimeout(() => { + this.jobs.delete(k); + run().catch((err) => { + logger.errorMessage( + `Scheduled job for token ${tokenId} method ${method} failed: ${err}` + ); + }); + }, delayMs); + + this.jobs.set(k, t); + return () => this.cancel(tokenId, method); + } + + /** Cancel a scheduled job. */ + cancel(tokenId: number, method: string): void { + const k = this.key(tokenId, method); + const t = this.jobs.get(k); + if (t) { + clearTimeout(t); + this.jobs.delete(k); + } + } + + /** Cancel all on shutdown. */ + cancelAll(): void { + for (const [, t] of this.jobs) clearTimeout(t); + this.jobs.clear(); + } +} diff --git a/Tokenization/backend/central-system/src/lib/utils/encrypt.ts b/Tokenization/backend/central-system/src/lib/utils/encrypt.ts new file mode 100644 index 000000000..f3a4831f9 --- /dev/null +++ b/Tokenization/backend/central-system/src/lib/utils/encrypt.ts @@ -0,0 +1,33 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +import {importSPKI, CompactEncrypt } from 'jose'; + + +/** * @description Encrypts data using a provided public key with RSA-OAEP-256 and A256GCM. + * @param publicKey - The public key in PEM format used for encryption. + * @param data - The plaintext data to be encrypted. + * @returns A promise that resolves to the encrypted data in JWE compact serialization format. + * @throws Will throw an error if the encryption process fails. + */ +async function encryptWithPublicKey(publicKey: string, data: string): Promise { + const pubKey = await importSPKI(publicKey, 'RSA-OAEP-256'); + const encoder = new TextEncoder(); + const encrypted = await new CompactEncrypt(encoder.encode(data)) + .setProtectedHeader({ alg:'RSA-OAEP-256', enc:'A256GCM', typ:'JWE' }) + .encrypt(pubKey); + return encrypted; +} + +export { encryptWithPublicKey }; \ No newline at end of file diff --git a/Tokenization/backend/central-system/src/lib/utils/event-req-types.d.ts b/Tokenization/backend/central-system/src/lib/utils/event-req-types.d.ts new file mode 100644 index 000000000..a18e1533b --- /dev/null +++ b/Tokenization/backend/central-system/src/lib/utils/event-req-types.d.ts @@ -0,0 +1,30 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +// Request and Response types for event handling + +import { VaultKvWritePayload } from '../../types/vault_types.js'; + +type SignTokenReq = { + data: { input: string }; + claims?: Record; +}; +type GetCredentialReq = { path: string; claims?: Record }; +type CreateOrUpdateCredentialReq = { + path: string; + body: VaultKvWritePayload; + claims?: Record; +}; + +export type { SignTokenReq, GetCredentialReq, CreateOrUpdateCredentialReq }; diff --git a/Tokenization/backend/central-system/src/lib/utils/events.ts b/Tokenization/backend/central-system/src/lib/utils/events.ts new file mode 100644 index 000000000..15bcbd1d8 --- /dev/null +++ b/Tokenization/backend/central-system/src/lib/utils/events.ts @@ -0,0 +1,22 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +/// Event types for vault operations +export enum EventType { + SIGN_TOKEN_VAULT = "SIGN_TOKEN_VAULT", + LOGIN_VAULT = "LOGIN_VAULT", + RENEW_VAULT_TOKEN = "RENEW_VAULT_TOKEN", + GET_CREDENTIAL_VAULT = "GET_CREDENTIAL_VAULT", + CREATE_OR_UPDATE_CREDENTIAL_VAULT = "CREATE_OR_UPDATE_CREDENTIAL_VAULT", +} diff --git a/Tokenization/backend/central-system/src/modules/CentralSystem.ts b/Tokenization/backend/central-system/src/modules/CentralSystem.ts index cb6b6f5a1..ee0095e1a 100644 --- a/Tokenization/backend/central-system/src/modules/CentralSystem.ts +++ b/Tokenization/backend/central-system/src/modules/CentralSystem.ts @@ -12,17 +12,26 @@ * or submit itself to any jurisdiction. */ -import { CentralSystemWrapper } from '../wrapper/CentralSystemWrapper.js'; -import { TokensController } from '../controllers/TokensController.js'; +import { CentralSystemWrapper } from '../lib/CentralSystemWrapper.js'; +import { ConnectionController } from '../controllers/ConnectionController.js'; +import { VaultController } from '../controllers/VaultController.js'; import path from 'path'; import { fileURLToPath } from 'url'; import { TokensGetService } from '../services/TokensGetService.js'; +import { VaultSignService } from '../services/VaultSignService.js'; +import { db } from '../lib/database/Database.js'; +import { SequelizeDatabase } from '../lib/database/SequelizeDatabase.js'; +import { VaultAuthService } from '../services/VaultAuthService.js'; +import { VaultCredentialsService } from '../services/VaultCredentialsService.js'; +import { EventType } from '../lib/utils/events.js'; +import { bus } from '../lib/event-bus/event-bus.js'; +import { LogManager } from '@aliceo2/web-ui'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); /* * CentralSystem class to handle token management. - * It includes methods to get tokens, create a new token, revoke tokens and provide tokens to relecant clients. + * It includes methods to get tokens, create a new token, revoke tokens and provide tokens to relevant clients. * The class uses a static Map to simulate a database of tokens. * The tokens are stored with a tokenId, validity status, and payload. */ @@ -33,24 +42,55 @@ class CentralSystem { number, { tokenId: number; validity: string; payload: string } >; - public readonly tokenController: TokensController; + private readonly _db: SequelizeDatabase; + private _logger; + + public readonly connectionController: ConnectionController; + public readonly vaultController: VaultController; + public constructor(wrapperPort: number) { - const tokensGetService = new TokensGetService(); + this._logger = LogManager.getLogger('CentralSystem'); + this._db = db; this._centralSystemWrapper = new CentralSystemWrapper( this.PROTO_PATH, wrapperPort ); + + this._centralSystemWrapper.listen(); this._fakeTokens = new Map([ [1, { tokenId: 1, validity: 'good', payload: 'payload1' }], [2, { tokenId: 2, validity: 'bad', payload: 'payload2' }], ]); - this.tokenController = new TokensController( - tokensGetService, + + this.connectionController = new ConnectionController( + new TokensGetService(), this._fakeTokens, this._centralSystemWrapper ); + + this.vaultController = new VaultController( + new VaultSignService(), + new VaultAuthService(), + new VaultCredentialsService() + ); + + this.vaultController.register(); + this.vaultController + .loginVault() + .then(() => {}) + .catch((error) => { + this._logger.errorMessage(`Error during Vault login: ${error.message}`); + }); + + setInterval(() => { + bus.emit(EventType.RENEW_VAULT_TOKEN, { + id: 'periodic-renew', + replyEvent: 'RENEW_VAULT_TOKEN:REPLY:periodic-renew', + payload: undefined, + }); + }, 6 * 1000 * 3600); // Renew every 6 hours } } diff --git a/Tokenization/backend/central-system/src/services/VaultAuthService.ts b/Tokenization/backend/central-system/src/services/VaultAuthService.ts new file mode 100644 index 000000000..b35d1c4d1 --- /dev/null +++ b/Tokenization/backend/central-system/src/services/VaultAuthService.ts @@ -0,0 +1,91 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +import { Agent } from 'https'; +import axios from 'axios'; + +// Define the structure of the login response +interface AuthResponse { + auth: { + client_token: string; + }; +} + +/** + * @description Service for authenticating with an external vault service. + */ +export class VaultAuthService { + /** + * @description Logs in to the vault service and retrieves a client token. + * @param url - The URL of the external vault service. + * @param agent - The HTTPS agent to use for the request. + * @param body - The body of the login request. + * @return A promise that resolves to the client token. + * @throws Will throw an error if the login fails. + */ + public async login( + url: string, + agent: Agent, + body: Buffer | string | NodeJS.ReadableStream | null + ): Promise { + try { + const resp = await axios.post(url, body, { + headers: { 'content-type': 'application/json' }, + httpsAgent: agent, + }); + return resp.data.auth.client_token; + } catch (err: any) { + const message = err?.response?.data + ? typeof err.response.data === 'string' + ? err.response.data + : JSON.stringify(err.response.data) + : err?.message ?? 'Unknown error'; + throw new Error(message); + } + } + + /** + * @description Renews the client token by sending a request to the vault service. + * @param url - The URL of the external vault service. + * @param token - The current client token. + * @param agent - The HTTPS agent to use for the request. + * @param body - The body of the renew request. + * @return A promise that resolves to the new client token. + * @throws Will throw an error if the renew fails. + */ + public async renew( + url: string, + token: string, + agent: Agent, + body: Buffer | string | NodeJS.ReadableStream | null + ): Promise { + try { + const resp = await axios.post(url, body, { + headers: { + 'content-type': 'application/json', + 'X-Vault-Token': token, + }, + httpsAgent: agent, + }); + return resp.data.auth.client_token; + } catch (err: any) { + const message = err?.response?.data + ? typeof err.response.data === 'string' + ? err.response.data + : JSON.stringify(err.response.data) + : err?.message ?? 'Unknown error'; + throw new Error(message); + } + } +} diff --git a/Tokenization/backend/central-system/src/services/VaultCredentialsService.ts b/Tokenization/backend/central-system/src/services/VaultCredentialsService.ts new file mode 100644 index 000000000..e1a236f23 --- /dev/null +++ b/Tokenization/backend/central-system/src/services/VaultCredentialsService.ts @@ -0,0 +1,87 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +import { Agent } from 'https'; +import axios from 'axios'; +import { VaultReadResponse } from '../types/vault_types.js'; + +/** + * * @description Service for retrieving credentials from an external vault service. + */ +export class VaultCredentialsService { + /** + * @description Retrieves credentials by sending a request to an external vault service. + * @param url - The URL of the external vault service. + * @param token - The JWT token for authentication. + * @param agent - The HTTPS agent to use for the request. + * @return A promise that resolves to the response from the vault service.\ + * @throws Will throw an error if retrieval fails. + */ + public async getCredential( + url: string, + token: string, + agent: Agent + ): Promise { + try { + const resp = await axios.get(url, { + headers: { + 'content-type': 'application/json', + 'X-Vault-Token': token, + }, + httpsAgent: agent, + }); + return resp.data; + } catch (err: any) { + const message = err?.response?.data + ? typeof err.response.data === 'string' + ? err.response.data + : JSON.stringify(err.response.data) + : err?.message ?? 'Unknown error'; + throw new Error(message); + } + } + + /** + * @description Creates or updates credentials by sending a request to an external vault service. + * @param url - The URL of the external vault service. + * @param token - The JWT token for authentication. + * @param agent - The HTTPS agent to use for the request. + * @param body - The body of the create/update request. + * @return A promise that resolves when the operation is complete. + * @throws Will throw an error if the operation fails. + */ + public async createOrUpdateCredential( + url: string, + token: string, + agent: Agent, + body: Buffer | string | NodeJS.ReadableStream | null + ): Promise { + try { + await axios.post(url, body, { + headers: { + 'content-type': 'application/json', + 'X-Vault-Token': token, + }, + httpsAgent: agent, + }); + } catch (err: any) { + const message = err?.response?.data + ? typeof err.response.data === 'string' + ? err.response.data + : JSON.stringify(err.response.data) + : err?.message ?? 'Unknown error'; + throw new Error(message); + } + } +} diff --git a/Tokenization/backend/central-system/src/services/VaultSignService.ts b/Tokenization/backend/central-system/src/services/VaultSignService.ts new file mode 100644 index 000000000..88a03b34f --- /dev/null +++ b/Tokenization/backend/central-system/src/services/VaultSignService.ts @@ -0,0 +1,62 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +import { Agent } from 'https'; +import axios from 'axios'; + +// Define the structure of the sign response +interface SignResponse { + data: { + signature: string; + }; +} + +/** + * @description Service for signing tokens using an external vault service. + */ +export class VaultSignService { + /** + * @description Signs a token by sending it to an external vault service. + * @param token - The JWT token to be signed. + * @param url - The URL of the external vault service. + * @param agent - The HTTPS agent to use for the request. + * @param body - The body of the sign request. + * @return A promise that resolves to the response from the vault service. + * @throws Will throw an error if signing fails. + */ + public async signToken( + url: string, + token: string, + agent: Agent, + body: Buffer | string | NodeJS.ReadableStream | null + ): Promise { + try { + const resp = await axios.post(url, body, { + headers: { + 'content-type': 'application/json', + 'X-Vault-Token': token, + }, + httpsAgent: agent, + }); + return resp.data.data.signature; + } catch (err: any) { + const message = err?.response?.data + ? typeof err.response.data === 'string' + ? err.response.data + : JSON.stringify(err.response.data) + : err?.message ?? 'Unknown error'; + throw new Error(message); + } + } +} diff --git a/Tokenization/backend/central-system/src/types/aliceo2__webui.d.ts b/Tokenization/backend/central-system/src/types/aliceo2__webui.d.ts index 1ff7043a4..c8fb5a474 100644 --- a/Tokenization/backend/central-system/src/types/aliceo2__webui.d.ts +++ b/Tokenization/backend/central-system/src/types/aliceo2__webui.d.ts @@ -12,7 +12,7 @@ * or submit itself to any jurisdiction. */ -declare module '@aliceo2/web-ui' { +declare module "@aliceo2/web-ui" { export const LogManager: any; export const InvalidInputError: any; export function updateAndSendExpressResponseFromNativeError( diff --git a/Tokenization/backend/central-system/src/types/vault_types.d.ts b/Tokenization/backend/central-system/src/types/vault_types.d.ts new file mode 100644 index 000000000..fbe78568c --- /dev/null +++ b/Tokenization/backend/central-system/src/types/vault_types.d.ts @@ -0,0 +1,64 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +// Type definitions for Vault-related operations +export interface SignPayload { + data: { input: string }; +} + +// Define the structure of the Vault read response +export interface VaultReadResponse { + data: { + data: { + foo: string; + }; + metadata: { + created_time: string; + custom_metadata: { + owner: string; + mission_critical: string; + }; + deletion_time: string; + destroyed: boolean; + version: number; + }; + }; +} + +// Define the structure of the Vault metadata response +export interface VaultMetadataResponse { + data: { + created_time: string; + custom_metadata: { + owner: string; + mission_critical: string; + }; + deletion_time: string; + destroyed: boolean; + version: number; + }; +} + +// Define the structure of the Vault KV write payload +export interface VaultKvWriteOptions { + cas?: number; +} + +// Define the structure of the Vault KV write payload +export interface VaultKvWritePayload { + options?: VaultKvWriteOptions; + data: { + [key: string]: string; + }; +} diff --git a/Tokenization/backend/central-system/tests/TokensController.spec.ts b/Tokenization/backend/central-system/tests/TokensController.spec.ts index ebbe16de5..8bdf16c39 100644 --- a/Tokenization/backend/central-system/tests/TokensController.spec.ts +++ b/Tokenization/backend/central-system/tests/TokensController.spec.ts @@ -14,7 +14,7 @@ import express from 'express'; import request from 'supertest'; -import { TokensController } from '../src/controllers/TokensController'; +import { ConnectionController } from '../src/controllers/ConnectionController'; import { TokensGetService } from '../src/services/TokensGetService'; // --- Fakes --- @@ -52,7 +52,7 @@ function makeApp(tokensMap?: Map) { const wrapper = new FakeWrapper(); const svc = new FakeTokensGetService(); - const controller = new TokensController(svc, fakeTokens, wrapper as any); + const controller = new ConnectionController(svc, fakeTokens, wrapper as any); const app = express(); app.use(express.json()); @@ -64,7 +64,7 @@ function makeApp(tokensMap?: Map) { // --- Tests --- -describe('TokensController', () => { +describe('ConnectionController', () => { test('GET /tokens returns transformed tokens', async () => { const { app } = makeApp(); const res = await request(app).get('/tokens-get'); @@ -81,6 +81,7 @@ describe('TokensController', () => { const res = await request(app) .post('/tokens/create') + .type('application/json') .send({ payload: 'new-payload-xyz' }); expect(res.status).toBe(201); @@ -95,7 +96,10 @@ describe('TokensController', () => { test('POST /tokens/create with empty payload -> 400', async () => { const { app } = makeApp(); - const res = await request(app).post('/tokens/create').send({ payload: '' }); + const res = await request(app) + .post('/tokens/create') + .type('application/json') + .send({ payload: '' }); expect(res.status).toBe(400); expect(res.body).toHaveProperty('message'); @@ -108,7 +112,10 @@ describe('TokensController', () => { // sanity: token 2 exists expect(tokens.has(2)).toBe(true); - const res = await request(app).post('/tokens/revoke').send({ id: 2 }); + const res = await request(app) + .post('/tokens/revoke') + .type('application/json') + .send({ id: 2 }); expect(res.status).toBe(204); expect(tokens.has(2)).toBe(false); @@ -119,7 +126,10 @@ describe('TokensController', () => { test('POST /tokens/revoke with non-existing id -> 400', async () => { const { app } = makeApp(); - const res = await request(app).post('/tokens/revoke').send({ id: 999 }); + const res = await request(app) + .post('/tokens/revoke') + .type('application/json') + .send({ id: 999 }); expect(res.status).toBe(400); expect(res.body).toHaveProperty('message'); diff --git a/Tokenization/backend/central-system/tests/VaultTests/VaultAuthService.spec.ts b/Tokenization/backend/central-system/tests/VaultTests/VaultAuthService.spec.ts new file mode 100644 index 000000000..a5f463401 --- /dev/null +++ b/Tokenization/backend/central-system/tests/VaultTests/VaultAuthService.spec.ts @@ -0,0 +1,80 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +import { jest } from '@jest/globals'; +import axios from 'axios'; +import { VaultAuthService } from '../../src/services/VaultAuthService'; + +jest.mock('axios'); + +describe('VaultAuthService', () => { + const agent: any = {}; + let service: VaultAuthService; + + beforeEach(() => { + jest.clearAllMocks(); + service = new VaultAuthService(); + }); + + it('login() returns client login token upon success', async () => { + const fakeToken = 's.fake-token'; + + (axios.post as jest.MockedFunction).mockResolvedValue({ + data: { auth: { client_token: fakeToken } }, + } as any); + + const body = JSON.stringify({ name: 'role' }); + const url = 'https://vault.local:9300/v1/auth/cert/login'; + + const result = await service.login(url, agent, body); + + expect(axios.post).toHaveBeenCalledWith(url, body, { + headers: { 'content-type': 'application/json' }, + httpsAgent: agent, + }); + expect(result).toBe(fakeToken); + }); + + it('login() throws error when response.ok === false', async () => { + (axios.post as jest.MockedFunction).mockRejectedValue({ + response: { data: 'error' }, + }); + + await expect( + service.login('https://vault.local:9300/v1/auth/cert/login', agent, '{}') + ).rejects.toThrow('error'); + }); + + it('renew() connects with proper token and reutrns renwed token', async () => { + const fakeToken = 's.renewed'; + + (axios.post as jest.MockedFunction).mockResolvedValue({ + data: { auth: { client_token: fakeToken } }, + } as any); + + const url = 'https://vault.local:9300/v1/auth/token/renew-self'; + const token = 's.old'; + + const result = await service.renew(url, token, agent, null); + + expect(axios.post).toHaveBeenCalledWith(url, null, { + headers: { + 'content-type': 'application/json', + 'X-Vault-Token': token, + }, + httpsAgent: agent, + }); + expect(result).toBe(fakeToken); + }); +}); diff --git a/Tokenization/backend/central-system/tests/VaultTests/VaultController.spec.ts b/Tokenization/backend/central-system/tests/VaultTests/VaultController.spec.ts new file mode 100644 index 000000000..1ab3b8cb6 --- /dev/null +++ b/Tokenization/backend/central-system/tests/VaultTests/VaultController.spec.ts @@ -0,0 +1,196 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +import { jest } from '@jest/globals'; +import { VaultController } from '../../src/controllers/VaultController'; +import { EventType } from '../../src/lib/utils/events'; +import { registerBusHandler } from '../../src/lib/event-bus/register-bus-handler'; + +jest.mock('../../src/lib/event-bus/register-bus-handler', () => ({ + registerBusHandler: jest.fn(), +})); + +const b64 = (s: string) => Buffer.from(s).toString('base64'); + +describe('VaultController', () => { + let tokenSignService: any; + let authService: any; + let credentialsService: any; + + beforeEach(() => { + jest.clearAllMocks(); + + // env for Vault + process.env.VAULT_ADDR = 'https://vault.local:9300'; + process.env.VAULT_AUTH_METHOD = 'cert'; + process.env.VAULT_ROLE = 'central-system'; + + process.env.VAULT_CACERT_B64 = b64('CA'); + process.env.VAULT_CENTRAL_SYSTEM_CERT_B64 = b64('CERT'); + process.env.VAULT_CENTRAL_SYSTEM_KEY_B64 = b64('KEY'); + + tokenSignService = { + signToken: jest + .fn<() => Promise>() + .mockResolvedValue('signedToken'), + }; + + authService = { + login: jest.fn<() => Promise>().mockResolvedValue('s.token'), + renew: jest + .fn<() => Promise>() + .mockResolvedValue('s.token-renewed'), + }; + + credentialsService = { + getCredential: jest + .fn<() => Promise<{ data: { foo: string } }>>() + .mockResolvedValue({ data: { foo: 'bar' } }), + createOrUpdateCredential: jest + .fn<() => Promise>() + .mockResolvedValue(undefined), + }; + }); + + test('missing TLS env vars', () => { + delete process.env.VAULT_CACERT_B64; + + expect( + () => + new VaultController(tokenSignService, authService, credentialsService) + ).toThrow('Missing required environment variables for TLS certificates.'); + }); + + test('loginVault() authService.login with proper body and URL', async () => { + const controller = new VaultController( + tokenSignService, + authService, + credentialsService + ); + + await controller.loginVault(); + + expect(authService.login).toHaveBeenCalledTimes(1); + const [url, agent, body] = authService.login.mock.calls[0]; + + expect(url).toBe('https://vault.local:9300/v1/auth/cert/login'); + expect(typeof agent).toBe('object'); + expect(JSON.parse(body as string)).toEqual({ name: 'central-system' }); + }); + + test('signToken() uses Vault login token and proper URL', async () => { + const controller = new VaultController( + tokenSignService, + authService, + credentialsService + ); + + await controller.loginVault(); + + const payload = { data: { sub: '123', foo: 'bar' } }; + + const result = await controller.signToken(payload as any); + + expect(tokenSignService.signToken).toHaveBeenCalledTimes(1); + const [url, token, agent, body] = tokenSignService.signToken.mock.calls[0]; + + expect(url).toBe('https://vault.local:9300/v1/transit/sign/tokenization-signing'); + expect(token).toBe('s.token'); + expect(typeof agent).toBe('object'); + expect(JSON.parse(body as string)).toEqual(payload.data); + expect(result).toBe('signedToken'); + }); + + test('getCredentialFromVault() creates proper URL and returns service output', async () => { + const controller = new VaultController( + tokenSignService, + authService, + credentialsService + ); + + (controller as any)._vaultAccessToken = 's.token'; + + const path = 'db/central-system'; + const result = await controller.getCredentialFromVault(path); + + expect(credentialsService.getCredential).toHaveBeenCalledTimes(1); + const [url, token, agent] = credentialsService.getCredential.mock.calls[0]; + + expect(url).toBe( + 'https://vault.local:9300/v1/tokenization/data/db/central-system' + ); + expect(token).toBe('s.token'); + expect(typeof agent).toBe('object'); + expect(result).toEqual({ data: { foo: 'bar' } }); + }); + + test('createOrUpdateCredentialInVault() calls createOrUpdateCredential with proper parameters', async () => { + const controller = new VaultController( + tokenSignService, + authService, + credentialsService + ); + + (controller as any)._vaultAccessToken = 's.token'; + + const path = 'db/central-system'; + const bodyObj = { data: { foo: 'bar' } }; + + await controller.createOrUpdateCredentialInVault(path, bodyObj); + + expect(credentialsService.createOrUpdateCredential).toHaveBeenCalledTimes( + 1 + ); + const [url, token, agent, body] = + credentialsService.createOrUpdateCredential.mock.calls[0]; + + expect(url).toBe( + 'https://vault.local:9300/v1/tokenization/data/db/central-system' + ); + expect(token).toBe('s.token'); + expect(typeof agent).toBe('object'); + expect(JSON.parse(body as string)).toEqual(bodyObj); + }); + + test('register() register events handlers for services', () => { + const controller = new VaultController( + tokenSignService, + authService, + credentialsService + ); + + controller.register(); + + expect(registerBusHandler).toHaveBeenCalledWith( + EventType.SIGN_TOKEN_VAULT, + expect.any(Function) + ); + expect(registerBusHandler).toHaveBeenCalledWith( + EventType.LOGIN_VAULT, + expect.any(Function) + ); + expect(registerBusHandler).toHaveBeenCalledWith( + EventType.RENEW_VAULT_TOKEN, + expect.any(Function) + ); + expect(registerBusHandler).toHaveBeenCalledWith( + EventType.GET_CREDENTIAL_VAULT, + expect.any(Function) + ); + expect(registerBusHandler).toHaveBeenCalledWith( + EventType.CREATE_OR_UPDATE_CREDENTIAL_VAULT, + expect.any(Function) + ); + }); +}); diff --git a/Tokenization/backend/central-system/tests/VaultTests/VaultCredentialsService.spec.ts b/Tokenization/backend/central-system/tests/VaultTests/VaultCredentialsService.spec.ts new file mode 100644 index 000000000..edaa4e700 --- /dev/null +++ b/Tokenization/backend/central-system/tests/VaultTests/VaultCredentialsService.spec.ts @@ -0,0 +1,101 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +import { jest } from '@jest/globals'; +import axios from 'axios'; +import { VaultCredentialsService } from '../../src/services/VaultCredentialsService'; + +jest.mock('axios'); + +describe('VaultCredentialsService', () => { + const agent: any = {}; + let service: VaultCredentialsService; + + beforeEach(() => { + jest.clearAllMocks(); + service = new VaultCredentialsService(); + }); + + it('getCredential() correct GET request with JSON answer', async () => { + (axios.get as jest.MockedFunction).mockResolvedValue({ + data: { data: { foo: 'bar', answer: 42 } }, + } as any); + const url = 'https://vault.local:9300/v1/secret/data/db/central-system'; + const token = 's.token'; + + const result = await service.getCredential(url, token, agent); + + expect(axios.get).toHaveBeenCalledTimes(1); + expect(axios.get).toHaveBeenCalledWith(url, { + headers: { + 'content-type': 'application/json', + 'X-Vault-Token': token, + }, + httpsAgent: agent, + }); + + expect(result).toEqual({ + data: { foo: 'bar', answer: 42 }, + }); + }); + + it('getCredential() HTTP error upon ok === false', async () => { + (axios.get as jest.MockedFunction).mockRejectedValue({ + response: { data: 'error' }, + }); + + const url = 'https://vault.local:9300/v1/secret/data/db/central-system'; + const token = 's.token'; + + await expect(service.getCredential(url, token, agent)).rejects.toThrow( + 'error' + ); + }); + + it('createOrUpdateCredential() correct POST request', async () => { + (axios.post as jest.MockedFunction).mockResolvedValue({ + data: {}, + } as any); + + const url = 'https://vault.local:9300/v1/secret/data/db/central-system'; + const token = 's.token'; + const bodyObj = { data: { foo: 'bar' } }; + const body = JSON.stringify(bodyObj); + + await service.createOrUpdateCredential(url, token, agent, body); + + expect(axios.post).toHaveBeenCalledTimes(1); + expect(axios.post).toHaveBeenCalledWith(url, body, { + headers: { + 'content-type': 'application/json', + 'X-Vault-Token': token, + }, + httpsAgent: agent, + }); + }); + + it('createOrUpdateCredential() HTTP error upon ok === false', async () => { + (axios.post as jest.MockedFunction).mockRejectedValue({ + response: { data: 'error' }, + }); + + const url = 'https://vault.local:9300/v1/secret/data/db/central-system'; + const token = 's.token'; + const body = JSON.stringify({ data: { foo: 'bar' } }); + + await expect( + service.createOrUpdateCredential(url, token, agent, body) + ).rejects.toThrow('error'); + }); +}); diff --git a/Tokenization/backend/central-system/tests/VaultTests/VaultIntegration.spec.ts b/Tokenization/backend/central-system/tests/VaultTests/VaultIntegration.spec.ts new file mode 100644 index 000000000..49ba9482b --- /dev/null +++ b/Tokenization/backend/central-system/tests/VaultTests/VaultIntegration.spec.ts @@ -0,0 +1,136 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +import fs from 'fs'; +import path from 'path'; +import { VaultController } from '../../src/controllers/VaultController'; +import { VaultAuthService } from '../../src/services/VaultAuthService'; +import { VaultSignService } from '../../src/services/VaultSignService'; + +import { VaultCredentialsService } from '../../src/services/VaultCredentialsService'; + +const b64 = (buf: Buffer) => buf.toString('base64'); + +function ensureVaultEnvFromFilesIfMissing() { + const backendRoot = path.resolve(__dirname, '..', '..'); + const repoRoot = path.resolve(backendRoot, '..', '..'); + + const caPath = path.join(repoRoot, 'docker', 'vault', 'ca.crt'); + const csCertPath = path.join( + repoRoot, + 'docker', + 'vault', + 'central-system.crt' + ); + const csKeyPath = path.join( + repoRoot, + 'docker', + 'vault', + 'central-system.key' + ); + + if (!process.env.VAULT_CACERT_B64 && fs.existsSync(caPath)) { + process.env.VAULT_CACERT_B64 = b64(fs.readFileSync(caPath)); + } + if (!process.env.VAULT_CENTRAL_SYSTEM_CERT_B64 && fs.existsSync(csCertPath)) { + process.env.VAULT_CENTRAL_SYSTEM_CERT_B64 = b64( + fs.readFileSync(csCertPath) + ); + } + if (!process.env.VAULT_CENTRAL_SYSTEM_KEY_B64 && fs.existsSync(csKeyPath)) { + process.env.VAULT_CENTRAL_SYSTEM_KEY_B64 = b64(fs.readFileSync(csKeyPath)); + } + + if (!process.env.VAULT_ADDR) { + process.env.VAULT_ADDR = 'https://vault.local:9300'; + } + if (!process.env.VAULT_AUTH_METHOD) { + process.env.VAULT_AUTH_METHOD = 'cert'; + } + if (!process.env.VAULT_ROLE) { + process.env.VAULT_ROLE = 'central-system'; + } +} + +describe('VaultController - integration with Vault', () => { + let controller: VaultController; + + beforeAll(async () => { + ensureVaultEnvFromFilesIfMissing(); + + controller = new VaultController( + new VaultSignService(), + new VaultAuthService(), + new VaultCredentialsService() + ); + + await controller.loginVault(); + }, 30000); + + beforeAll(async () => { + ensureVaultEnvFromFilesIfMissing(); + + console.log('DEBUG VAULT ENVS (integration):', { + CA: process.env.VAULT_CACERT_B64 ? 'set' : 'missing', + CERT: process.env.VAULT_CENTRAL_SYSTEM_CERT_B64 ? 'set' : 'missing', + KEY: process.env.VAULT_CENTRAL_SYSTEM_KEY_B64 ? 'set' : 'missing', + }); + + controller = new VaultController( + new VaultSignService(), + new VaultAuthService(), + new VaultCredentialsService() + ); + + await controller.loginVault(); + }, 30000); + it('renews Vault token successfully', async () => { + await controller.renewVaultToken(); + }, 20000); + + it('creates/updates and then reads back a secret from KV', async () => { + const pathInVault = 'integration-test/app-config'; + + const body = { + data: { + foo: 'bar', + answer: '42', + }, + }; + + await controller.createOrUpdateCredentialInVault(pathInVault, body); + + const secret = await controller.getCredentialFromVault(pathInVault); + + const payload = + (secret as any).data?.data ?? (secret as any).data ?? secret; + + expect(payload.foo).toBe('bar'); + expect(payload.answer).toBe('42'); + }, 20000); + + it('signs a payload using Transit engine', async () => { + const payload = { + data: { + sub: 'user-123', + role: 'integration-test', + }, + }; + + const signature = await controller.signToken(payload as any); + + expect(typeof signature).toBe('string'); + expect(signature.length).toBeGreaterThan(0); + }, 20000); +}); diff --git a/Tokenization/backend/central-system/tests/VaultTests/VaultSignService.spec.ts b/Tokenization/backend/central-system/tests/VaultTests/VaultSignService.spec.ts new file mode 100644 index 000000000..4e702d58f --- /dev/null +++ b/Tokenization/backend/central-system/tests/VaultTests/VaultSignService.spec.ts @@ -0,0 +1,67 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +import { jest } from '@jest/globals'; +import axios from 'axios'; +import { VaultSignService } from '../../src/services/VaultSignService'; + +jest.mock('axios'); + +describe('VaultSignService', () => { + const agent: any = {}; + let service: VaultSignService; + + beforeEach(() => { + jest.clearAllMocks(); + service = new VaultSignService(); + }); + + it('signToken() send proper request and return signature', async () => { + const signature = 'vault:v1:abcdef'; + (axios.post as jest.MockedFunction).mockResolvedValue({ + data: { data: { signature } }, + } as any); + + const url = 'https://vault.local:9300/v1/transit/sign/signing-key'; + const token = 's.token'; + const body = JSON.stringify({ data: { sub: '123' } }); + + const result = await service.signToken(url, token, agent, body); + + expect(axios.post).toHaveBeenCalledTimes(1); + expect(axios.post).toHaveBeenCalledWith(url, body, { + headers: { + 'content-type': 'application/json', + 'X-Vault-Token': token, + }, + httpsAgent: agent, + }); + + expect(result).toBe(signature); + }); + + it('signToken() throws error when response.ok === false', async () => { + (axios.post as jest.MockedFunction).mockRejectedValue({ + response: { data: 'error' }, + }); + + const url = 'https://vault.local:9300/v1/transit/sign/signing-key'; + const token = 's.token'; + const body = JSON.stringify({ data: { sub: '123' } }); + + await expect(service.signToken(url, token, agent, body)).rejects.toThrow( + 'error' + ); + }); +}); diff --git a/Tokenization/docker-compose.dev.yml b/Tokenization/docker-compose.dev.yml new file mode 100644 index 000000000..67b867751 --- /dev/null +++ b/Tokenization/docker-compose.dev.yml @@ -0,0 +1,125 @@ +services: + vault: + image: hashicorp/vault:latest + container_name: vault + ports: + - '127.0.0.1:8200:9300' + - '127.0.0.1:8201:9301' + environment: + VAULT_LOCAL_CONFIG: | + ui = true + disable_mlock = true + cluster_name = "dev-raft" + + api_addr = "https://vault.local:8200" + cluster_addr = "https://vault.local:8201" + + listener "tcp" { + address = "0.0.0.0:9300" + cluster_address = "0.0.0.0:9301" + tls_cert_file = "/vault/config/vault.crt" + tls_key_file = "/vault/config/vault.key" + tls_client_ca_file = "/vault/config/ca.crt" + } + + storage "raft" { + path = "/vault/data" + node_id = "node1" + } + entrypoint: + - /bin/sh + - -lc + - | + set -e + docker-entrypoint.sh >/dev/null 2>&1 || true + exec vault server -config=/vault/config/local.json + networks: + default: + aliases: + - vault.local + volumes: + - type: volume + source: vault-data + target: /vault/data + + - ./docker/vault/vault.crt:/vault/config/vault.crt:ro + - ./docker/vault/vault.key:/vault/config/vault.key:ro + - ./docker/ca/ca.crt:/vault/config/ca.crt:ro + cap_add: + - IPC_LOCK + restart: unless-stopped + vault-setup: + image: hashicorp/vault:latest + depends_on: + vault: + condition: service_started + environment: + VAULT_ADDR: https://vault.local:9300 + VAULT_TLS_SERVER_NAME: vault.local + volumes: + - vault-data:/vault/data + - ./docker/vault/vault.crt:/vault/config/vault.crt:ro + - ./docker/vault/vault.key:/vault/config/vault.key:ro + - ./docker/vault/ca.crt:/vault/config/ca.crt:ro + - ./docker/vault/vault-setup.sh:/vault-setup.sh:ro + - ./docker/vault/central-system.crt:/vault/config/central-system-client.crt:ro + entrypoint: ['/bin/sh', '-lc'] + command: ['/vault-setup.sh'] + restart: 'on-failure' + + database: + image: 'mariadb:10.5.9' + container_name: tokenization-database + environment: + MYSQL_ROOT_PASSWORD: 'cern' + ports: + - '3306:3306' + volumes: + - type: volume + source: database-data + target: /var/lib/mysql + - type: bind + read_only: true + source: ./docker/database/configuration + target: /etc/mysql/conf.d + - type: bind + read_only: true + source: ./docker/database/populate + target: /docker-entrypoint-initdb.d + healthcheck: + test: + [ + 'CMD', + 'mysqladmin', + 'ping', + '-h', + 'localhost', + '--user=root', + '--password=cern', + ] + interval: 10s + timeout: 5s + retries: 12 + start_period: 20s + restart: unless-stopped + + central-tests: + image: node:22-alpine + working_dir: /var/workspace/central-system + volumes: + - ./backend:/var/workspace + env_file: + - ./backend/central-system/.env + environment: + VAULT_ADDR: https://vault.local:9300 + VAULT_AUTH_METHOD: cert + VAULT_ROLE: central-system + + command: ['npm', 'run', 'test:vault:integration'] + depends_on: + vault-setup: + condition: service_completed_successfully + +volumes: + database-data: + vault-data: diff --git a/Tokenization/docker-compose.test.yml b/Tokenization/docker-compose.test.yml index 695b63d6d..64a6f513d 100644 --- a/Tokenization/docker-compose.test.yml +++ b/Tokenization/docker-compose.test.yml @@ -5,12 +5,13 @@ services: volumes: - ./backend:/var/workspace command: ['npm', 'install', '--no-save', '--silent'] - backend: image: node:22-alpine working_dir: /var/workspace/central-system volumes: - ./backend:/var/workspace + env_file: + - ./backend/central-system/.env command: ['npm', 'run', 'dev'] healthcheck: interval: 1s @@ -24,6 +25,10 @@ services: depends_on: install-backend: condition: service_completed_successfully + database: + condition: service_healthy + vault-setup: + condition: service_completed_successfully prod-container: build: @@ -35,6 +40,8 @@ services: interval: 5s timeout: 3s retries: 5 + ports: + - 8080:80 depends_on: backend: condition: service_healthy @@ -49,3 +56,4 @@ services: depends_on: prod-container: condition: service_healthy + diff --git a/Tokenization/docker-compose.yml b/Tokenization/docker-compose.yml index df50f3c06..bf3811c50 100644 --- a/Tokenization/docker-compose.yml +++ b/Tokenization/docker-compose.yml @@ -10,6 +10,8 @@ services: working_dir: /var/workspace/central-system volumes: - ./backend:/var/workspace + env_file: + - ./backend/central-system/.env command: ['npm', 'run', 'dev'] healthcheck: interval: 1s @@ -23,6 +25,10 @@ services: depends_on: install-backend: condition: service_completed_successfully + database: + condition: service_healthy + vault-setup: + condition: service_completed_successfully install-webapp: image: node:22-alpine diff --git a/Tokenization/docker/database/configuration/my.cnf b/Tokenization/docker/database/configuration/my.cnf new file mode 100644 index 000000000..d031c0009 --- /dev/null +++ b/Tokenization/docker/database/configuration/my.cnf @@ -0,0 +1,14 @@ +[client] +default-character-set = utf8mb4 + +[mysql] +default-character-set = utf8mb4 + +[mysqld] +lower_case_table_names = 0 + +# Ensure utf8mb4 everywhere +init_connect = 'SET NAMES utf8mb4; SET collation_connection = utf8mb4_unicode_ci' +character-set-server = utf8mb4 +collation-server = utf8mb4_unicode_ci +skip-character-set-client-handshake diff --git a/Tokenization/docker/database/populate/01-create-database.sql b/Tokenization/docker/database/populate/01-create-database.sql new file mode 100644 index 000000000..150b7af3b --- /dev/null +++ b/Tokenization/docker/database/populate/01-create-database.sql @@ -0,0 +1,3 @@ +CREATE DATABASE IF NOT EXISTS `tokenization` + CHARACTER SET utf8mb4 + COLLATE utf8mb4_unicode_ci; \ No newline at end of file diff --git a/Tokenization/docker/database/populate/02-create-user.sql b/Tokenization/docker/database/populate/02-create-user.sql new file mode 100644 index 000000000..9a531c438 --- /dev/null +++ b/Tokenization/docker/database/populate/02-create-user.sql @@ -0,0 +1,5 @@ +CREATE USER IF NOT EXISTS 'central-system'@'%' IDENTIFIED BY 'cern'; + +GRANT ALL PRIVILEGES ON `tokenization`.* TO 'central-system'@'%'; + +FLUSH PRIVILEGES; \ No newline at end of file diff --git a/Tokenization/docker/vault/vault-setup.sh b/Tokenization/docker/vault/vault-setup.sh new file mode 100755 index 000000000..876573afa --- /dev/null +++ b/Tokenization/docker/vault/vault-setup.sh @@ -0,0 +1,144 @@ +#!/usr/bin/env sh +set -euo pipefail + +export VAULT_ADDR="${VAULT_ADDR:-https://vault.local:9300}" +export VAULT_CACERT=${VAULT_CACERT:-/vault/config/ca.crt} +export VAULT_TLS_SERVER_NAME=${VAULT_TLS_SERVER_NAME:-vault.local} + +echo "[vault-setup] VAULT_ADDR=$VAULT_ADDR" +echo "[vault-setup] VAULT_CACERT=$VAULT_CACERT" +echo "[vault-setup] VAULT_TLS_SERVER_NAME=$VAULT_TLS_SERVER_NAME" + +echo "[vault-setup] Waiting for Vault to respond..." +for i in $(seq 1 60); do + if vault status >/dev/null 2>&1; then + break + fi + sleep 1 +done + +status_json=$(vault status -format=json || true) +initialized=$(echo "$status_json" | sed -n 's/.*"initialized":[[:space:]]*\(true\|false\).*/\1/p') +sealed=$(echo "$status_json" | sed -n 's/.*"sealed":[[:space:]]*\(true\|false\).*/\1/p') + +echo "[vault-setup] current status: initialized=$initialized, sealed=$sealed" + +UNSEAL_KEY="" +ROOT_TOKEN="" + +if [ "$initialized" = "false" ]; then + echo "[vault-setup] Vault NOT initialized. Running operator init (text output)..." + + init_output=$(vault operator init -key-shares=1 -key-threshold=1) + + echo "$init_output" | sed -n '1,50p' + + UNSEAL_KEY=$(printf '%s\n' "$init_output" | awk '/Unseal Key 1:/ {print $NF}') + ROOT_TOKEN=$(printf '%s\n' "$init_output" | awk '/Initial Root Token:/ {print $NF}') + + if [ -z "$UNSEAL_KEY" ] || [ -z "$ROOT_TOKEN" ]; then + echo "[vault-setup] ERROR: Failed to parse UNSEAL_KEY or ROOT_TOKEN from vault operator init output." + exit 1 + fi + + mkdir -p /vault/data + echo "$UNSEAL_KEY" > /vault/data/unseal_key + echo "$ROOT_TOKEN" > /vault/data/root_token + + export VAULT_TOKEN="$ROOT_TOKEN" +else + echo "[vault-setup] Vault already initialized." + + if [ -f /vault/data/root_token ]; then + ROOT_TOKEN=$(cat /vault/data/root_token) + export VAULT_TOKEN="$ROOT_TOKEN" + echo "[vault-setup] Loaded ROOT_TOKEN from /vault/data/root_token" + else + echo "[vault-setup] WARNING: /vault/data/root_token missing. Some config may fail." + fi + + if [ -f /vault/data/unseal_key ]; then + UNSEAL_KEY=$(cat /vault/data/unseal_key) + echo "[vault-setup] Loaded UNSEAL_KEY from /vault/data/unseal_key" + else + echo "[vault-setup] WARNING: /vault/data/unseal_key missing. Cannot auto-unseal if sealed." + fi +fi + + +status_json=$(vault status -format=json || true) +sealed=$(echo "$status_json" | sed -n 's/.*"sealed":[[:space:]]*\(true\|false\).*/\1/p') +echo "[vault-setup] status after init: sealed=$sealed" + +if [ "$sealed" = "true" ]; then + if [ -z "${UNSEAL_KEY:-}" ]; then + echo "[vault-setup] ERROR: Vault is sealed and UNSEAL_KEY is not available." + exit 1 + fi + + echo "[vault-setup] Unsealing Vault..." + vault operator unseal "$UNSEAL_KEY" +else + echo "[vault-setup] Vault already unsealed." +fi + +echo "[vault-setup] Ensuring Vault is reachable with token..." +if [ -n "${VAULT_TOKEN:-}" ]; then + vault token lookup >/dev/null 2>&1 || echo "[vault-setup] WARNING: VAULT_TOKEN may be invalid." +else + echo "[vault-setup] WARNING: VAULT_TOKEN not set – policy/config steps may fail." +fi + +#################################### +# VAULT CONFIGURATION STEPS +#################################### + +if [ -n "${VAULT_TOKEN:-}" ]; then + echo "[vault-setup] Enabling secrets engines..." + vault secrets enable transit 2>/dev/null || echo "[vault-setup] transit already enabled" + vault secrets enable -path=tokenization kv-v2 2>/dev/null || echo "[vault-setup] tokenization kv-v2 already enabled" + + echo "[vault-setup] Creating transit key for tokenization..." + vault write transit/keys/tokenization-signing type="rsa-2048" 2>/dev/null || echo "[vault-setup] transit key already exists" + + echo "[vault-setup] Creating central-system policy..." + vault policy write central-system - </dev/null || echo "[vault-setup] cert auth already enabled" + + vault write auth/cert/certs/central-system \ + display_name="central-system" \ + policies="central-system" \ + certificate=@/vault/config/central-system-client.crt \ + ttl=24h 2>/dev/null || echo "[vault-setup] central-system cert role already exists" +else + echo "[vault-setup] Skipping policy / auth config – no VAULT_TOKEN." +fi + +echo "[vault-setup] Initialization + unseal + config finished OK." +exit 0 diff --git a/Tokenization/package-lock.json b/Tokenization/package-lock.json new file mode 100644 index 000000000..8f0b549d6 --- /dev/null +++ b/Tokenization/package-lock.json @@ -0,0 +1,103 @@ +{ + "name": "Tokenization", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "node-fetch": "^3.3.2" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + } + } +} diff --git a/Tokenization/package.json b/Tokenization/package.json new file mode 100644 index 000000000..179737248 --- /dev/null +++ b/Tokenization/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "node-fetch": "^3.3.2" + } +} diff --git a/Tokenization/scripts/actions-certificates-creation.sh b/Tokenization/scripts/actions-certificates-creation.sh new file mode 100644 index 000000000..4f0a366d7 --- /dev/null +++ b/Tokenization/scripts/actions-certificates-creation.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +set -euo pipefail + +VAULT_DIR="./Tokenization/docker/vault" +BACKEND_ENV="./Tokenization/backend/central-system/.env" + +mkdir -p "$VAULT_DIR" +mkdir -p "$(dirname "$BACKEND_ENV")" + +echo "[Vault CI] Generating test CA..." +openssl genrsa -out "$VAULT_DIR/ca.key" 4096 +openssl req -x509 -new -nodes -key "$VAULT_DIR/ca.key" -sha256 -days 3650 \ + -out "$VAULT_DIR/ca.crt" \ + -subj "/CN=TestVaultCA" + +echo "[Vault CI] Generating Vault server key/cert..." +openssl genrsa -out "$VAULT_DIR/vault.key" 2048 +openssl req -new -key "$VAULT_DIR/vault.key" -out "$VAULT_DIR/vault.csr" \ + -subj "/CN=vault.local" + +cat > "$VAULT_DIR/vault.ext" < "$BACKEND_ENV" <