diff --git a/.eslintrc.json b/.eslintrc.json index f2d9db8eb..09ee628c2 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,5 +1,5 @@ { - "parser": "@babel/eslint-parser", + "parser": "@typescript-eslint/parser", "env": { "node": true, "browser": true, @@ -9,13 +9,23 @@ }, "extends": [ "eslint:recommended", + "plugin:@typescript-eslint/recommended", "plugin:react/recommended", "google", "prettier", "plugin:json/recommended" ], - "overrides": [], + "overrides": [ + { + "files": ["test/**/*.js", "**/*.json"], + "parser": "espree", + "rules": { + "@typescript-eslint/no-unused-expressions": "off" + } + } + ], "parserOptions": { + "project": "./tsconfig.json", "requireConfigFile": false, "ecmaVersion": 12, "sourceType": "module", @@ -27,11 +37,15 @@ "presets": ["@babel/preset-react"] } }, - "plugins": ["react", "prettier"], + "plugins": ["@typescript-eslint", "react", "prettier"], "rules": { "react/prop-types": "off", "require-jsdoc": "off", - "no-async-promise-executor": "off" + "no-async-promise-executor": "off", + "@typescript-eslint/no-explicit-any": "warn", // temporary until TS refactor is complete + "@typescript-eslint/no-unused-vars": "off", // temporary until TS refactor is complete + "@typescript-eslint/no-require-imports": "off", // prevents error on old "require" imports + "@typescript-eslint/no-unused-expressions": "off" // prevents error on test "expect" expressions }, "settings": { "react": { diff --git a/.github/workflows/unused-dependencies.yml b/.github/workflows/unused-dependencies.yml index 3aa9dda66..39071e270 100644 --- a/.github/workflows/unused-dependencies.yml +++ b/.github/workflows/unused-dependencies.yml @@ -1,8 +1,8 @@ name: 'Unused Dependencies' on: [pull_request] -permissions: - contents: read +permissions: + contents: read jobs: unused-dependecies: @@ -20,11 +20,10 @@ jobs: with: node-version: '18.x' - name: 'Run depcheck' - run: | - npx depcheck --skip-missing --ignores="@babel/*,@commitlint/*,eslint,eslint-*,husky,mocha,concurrently,nyc,prettier" + run: | + npx depcheck --skip-missing --ignores="tsx,@babel/*,@commitlint/*,eslint,eslint-*,husky,mocha,ts-mocha,ts-node,concurrently,nyc,prettier,typescript,tsconfig-paths,vite-tsconfig-paths" echo $? if [[ $? == 1 ]]; then echo "Unused dependencies or devDependencies found" exit 1 fi - diff --git a/index.js b/index.js deleted file mode 100755 index c86c0425e..000000000 --- a/index.js +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env node -/* eslint-disable max-len */ -const argv = require('yargs/yargs')(process.argv.slice(2)) - .usage('Usage: $0 [options]') - .options({ - validate: { - description: - 'Check the proxy.config.json file in the current working directory for validation errors.', - required: false, - alias: 'v', - }, - config: { - description: 'Path to custom git-proxy configuration file.', - default: 'proxy.config.json', - required: false, - alias: 'c', - }, - }) - .strict().argv; - -const config = require('./src/config/file'); -config.configFile = argv.c ? argv.c : undefined; - -if (argv.v) { - const fs = require('fs'); - - if (!fs.existsSync(config.configFile)) { - console.error( - `Config file ${config.configFile} doesn't exist, nothing to validate! Did you forget -c/--config?`, - ); - process.exit(1); - } - - config.validate(); - console.log(`${config.configFile} is valid`); - process.exit(0); -} - -config.validate(); - -const proxy = require('./src/proxy'); -const service = require('./src/service'); - -proxy.start(); -service.start(); - -module.exports.proxy = proxy; -module.exports.service = service; diff --git a/index.ts b/index.ts new file mode 100755 index 000000000..880ccfe02 --- /dev/null +++ b/index.ts @@ -0,0 +1,51 @@ +#!/usr/bin/env tsx +/* eslint-disable max-len */ +import yargs from 'yargs'; +import { hideBin } from 'yargs/helpers'; +import * as fs from 'fs'; +import { configFile, setConfigFile, validate } from './src/config/file'; +import proxy from './src/proxy'; +import service from './src/service'; + +const argv = yargs(hideBin(process.argv)) + .usage('Usage: $0 [options]') + .options({ + validate: { + description: + 'Check the proxy.config.json file in the current working directory for validation errors.', + required: false, + alias: 'v', + type: 'boolean', + }, + config: { + description: 'Path to custom git-proxy configuration file.', + default: 'proxy.config.json', + required: false, + alias: 'c', + type: 'string', + }, + }) + .strict() + .parseSync(); + +setConfigFile(argv.c as string || ""); + +if (argv.v) { + if (!fs.existsSync(configFile)) { + console.error( + `Config file ${configFile} doesn't exist, nothing to validate! Did you forget -c/--config?`, + ); + process.exit(1); + } + + validate(); + console.log(`${configFile} is valid`); + process.exit(0); +} + +validate(); + +proxy.start(); +service.start(); + +export { proxy, service }; diff --git a/package-lock.json b/package-lock.json index 2c53d8a2f..3052eaddc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -63,6 +63,14 @@ "@babel/preset-react": "^7.22.5", "@commitlint/cli": "^19.0.0", "@commitlint/config-conventional": "^19.0.0", + "@types/express": "^5.0.1", + "@types/express-http-proxy": "^1.6.6", + "@types/lodash": "^4.17.15", + "@types/mocha": "^10.0.10", + "@types/node": "^22.13.5", + "@types/yargs": "^17.0.33", + "@typescript-eslint/eslint-plugin": "^8.26.1", + "@typescript-eslint/parser": "^8.26.1", "@vitejs/plugin-react": "^4.0.2", "chai": "^4.2.0", "chai-http": "^4.3.0", @@ -74,12 +82,18 @@ "eslint-plugin-prettier": "^5.0.0", "eslint-plugin-react": "^7.21.5", "eslint-plugin-standard": "^5.0.0", + "eslint-plugin-typescript": "^0.14.0", "husky": "^9.0.0", - "mocha": "^10.2.0", + "mocha": "^10.8.2", "nyc": "^17.0.0", "prettier": "^3.0.0", "sinon": "^19.0.2", - "vite": "^4.4.2" + "ts-mocha": "^11.1.0", + "ts-node": "^10.9.2", + "tsx": "^4.19.3", + "typescript": "^5.7.3", + "vite": "4.5.5", + "vite-tsconfig-paths": "^5.1.4" }, "optionalDependencies": { "@esbuild/darwin-arm64": "^0.18.20", @@ -1652,6 +1666,30 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@cypress/request": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.6.tgz", @@ -1715,6 +1753,22 @@ "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==" }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.1.tgz", + "integrity": "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/android-arm": { "version": "0.18.20", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", @@ -1984,6 +2038,22 @@ "node": ">=18" } }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.1.tgz", + "integrity": "sha512-O96poM2XGhLtpTh+s4+nP7YCCAfb4tJNRVZHfIE7dgmax+yMP2WgMd2OecBuaATHKTHsLWHQeuaxMRnCsH8+5g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/netbsd-x64": { "version": "0.18.20", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", @@ -2001,6 +2071,22 @@ "node": ">=12" } }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.1.tgz", + "integrity": "sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/openbsd-x64": { "version": "0.18.20", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", @@ -3492,6 +3578,34 @@ "node": ">=14.0.0" } }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "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", @@ -3533,12 +3647,33 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, "node_modules/@types/chai": { "version": "4.3.11", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.11.tgz", "integrity": "sha512-qQR1dr2rGIHYlJulmr8Ioq3De0Le9E4MJ5AiaeAETJJpndT1uUNHsGFK3L/UIu+rbkQSdj8J/w2bCsBZc/Y5fQ==", "dev": true }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/conventional-commits-parser": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/conventional-commits-parser/-/conventional-commits-parser-5.0.0.tgz", @@ -3555,12 +3690,74 @@ "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", "dev": true }, + "node_modules/@types/express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.1.tgz", + "integrity": "sha512-UZUw8vjpWFXuDnjFTh7/5c2TWDlQqeXHi6hcN7F2XSVT5P+WmUnnbFS3KA6Jnc6IsEqI2qCVu2bK0R0J4A8ZQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-http-proxy": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/@types/express-http-proxy/-/express-http-proxy-1.6.6.tgz", + "integrity": "sha512-J8ZqHG76rq1UB716IZ3RCmUhg406pbWxsM3oFCFccl5xlWUPzoR4if6Og/cE4juK8emH0H9quZa5ltn6ZdmQJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz", + "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.16", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.16.tgz", + "integrity": "sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mocha": { + "version": "10.0.10", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", + "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", + "dev": true + }, "node_modules/@types/node": { - "version": "20.10.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.7.tgz", - "integrity": "sha512-fRbIKb8C/Y2lXxB5eVMj4IU7xpdox0Lh8bUPEdtLysaylsml1hOOx1+STloRs/B9nf7C6kPRmmg/V7aQW7usNg==", + "version": "22.13.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", + "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.20.0" } }, "node_modules/@types/prop-types": { @@ -3568,6 +3765,20 @@ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==" }, + "node_modules/@types/qs": { + "version": "6.9.18", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", + "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/react": { "version": "17.0.74", "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.74.tgz", @@ -3596,6 +3807,29 @@ "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==" }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, "node_modules/@types/sinonjs__fake-timers": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz", @@ -3632,6 +3866,23 @@ "@types/webidl-conversions": "*" } }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/yauzl": { "version": "2.10.3", "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", @@ -3642,143 +3893,388 @@ "@types/node": "*" } }, - "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true - }, - "node_modules/@vitejs/plugin-react": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz", - "integrity": "sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==", + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.26.1.tgz", + "integrity": "sha512-2X3mwqsj9Bd3Ciz508ZUtoQQYpOhU/kWoUqIf49H8Z0+Vbh6UF/y0OEYp0Q0axOGzaBGs7QxRwq0knSQ8khQNA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.26.0", - "@babel/plugin-transform-react-jsx-self": "^7.25.9", - "@babel/plugin-transform-react-jsx-source": "^7.25.9", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.14.2" + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.26.1", + "@typescript-eslint/type-utils": "8.26.1", + "@typescript-eslint/utils": "8.26.1", + "@typescript-eslint/visitor-keys": "8.26.1", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.0.1" }, "engines": { - "node": "^14.18.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" - }, - "node_modules/abstract-logging": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", - "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==" - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "node_modules/@typescript-eslint/parser": { + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.26.1.tgz", + "integrity": "sha512-w6HZUV4NWxqd8BdeFf81t07d7/YV9s7TCWrQQbG5uhuvGUAW+fq1usZ1Hmz9UPNLniFnD8GLSsDpjP0hm1S4lQ==", + "dev": true, + "license": "MIT", "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" + "@typescript-eslint/scope-manager": "8.26.1", + "@typescript-eslint/types": "8.26.1", + "@typescript-eslint/typescript-estree": "8.26.1", + "@typescript-eslint/visitor-keys": "8.26.1", + "debug": "^4.3.4" }, "engines": { - "node": ">= 0.6" - } - }, - "node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", - "dev": true, - "bin": { - "acorn": "bin/acorn" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/activedirectory2": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/activedirectory2/-/activedirectory2-2.2.0.tgz", - "integrity": "sha512-uGbw74xttFG6hgocU8T1a0oDofLsyTp44BPTn42JN5C2QlyO5kRl2E7ZoUdfpFzV+yxhaQTKI+8QqRB5HONYvA==", - "dependencies": { - "abstract-logging": "^2.0.0", - "async": "^3.1.0", - "ldapjs": "^2.3.3", - "merge-options": "^2.0.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, - "engines": { - "node": ">=4.0" + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.26.1.tgz", + "integrity": "sha512-6EIvbE5cNER8sqBu6V7+KeMZIC1664d2Yjt+B9EWUXrsyWpxx4lEZrmvxgSKRC6gX+efDL/UY9OpPZ267io3mg==", "dev": true, + "license": "MIT", "dependencies": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" + "@typescript-eslint/types": "8.26.1", + "@typescript-eslint/visitor-keys": "8.26.1" }, "engines": { - "node": ">=8" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "node_modules/@typescript-eslint/type-utils": { + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.26.1.tgz", + "integrity": "sha512-Kcj/TagJLwoY/5w9JGEFV0dclQdyqw9+VMndxOJKtoFSjfZhLXhYjzsQEeyza03rwHx2vFEGvrJWJBXKleRvZg==", "dev": true, "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" + "@typescript-eslint/typescript-estree": "8.26.1", + "@typescript-eslint/utils": "8.26.1", + "debug": "^4.3.4", + "ts-api-utils": "^2.0.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/ansi-colors": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "node_modules/@typescript-eslint/types": { + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.26.1.tgz", + "integrity": "sha512-n4THUQW27VmQMx+3P+B0Yptl7ydfceUj4ON/AQILAASwgYdZ/2dhfymRMh5egRUrvK5lSmaOm77Ry+lmXPOgBQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=6" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.26.1.tgz", + "integrity": "sha512-yUwPpUHDgdrv1QJ7YQal3cMVBGWfnuCdKbXw1yyjArax3353rEJP1ZA+4F8nOlQ3RfS2hUN/wze3nlY+ZOhvoA==", "dev": true, + "license": "MIT", "dependencies": { - "type-fest": "^0.21.3" + "@typescript-eslint/types": "8.26.1", + "@typescript-eslint/visitor-keys": "8.26.1", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.0.1" }, "engines": { - "node": ">=8" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.26.1.tgz", + "integrity": "sha512-V4Urxa/XtSUroUrnI7q6yUTD3hDtfJ2jzVfeT3VK0ciizfK2q/zGC0iDh1lFMUZR8cImRrep6/q0xd/1ZGPQpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.26.1", + "@typescript-eslint/types": "8.26.1", + "@typescript-eslint/typescript-estree": "8.26.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.26.1.tgz", + "integrity": "sha512-AjOC3zfnxd6S4Eiy3jwktJPclqhFHNyd8L6Gycf9WUPoKZpgM5PjkxY1X7uSy61xVpiJDhhk7XT2NVsN3ALTWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.26.1", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz", + "integrity": "sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.26.0", + "@babel/plugin-transform-react-jsx-self": "^7.25.9", + "@babel/plugin-transform-react-jsx-source": "^7.25.9", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.14.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/activedirectory2": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/activedirectory2/-/activedirectory2-2.2.0.tgz", + "integrity": "sha512-uGbw74xttFG6hgocU8T1a0oDofLsyTp44BPTn42JN5C2QlyO5kRl2E7ZoUdfpFzV+yxhaQTKI+8QqRB5HONYvA==", + "dependencies": { + "abstract-logging": "^2.0.0", + "async": "^3.1.0", + "ldapjs": "^2.3.3", + "merge-options": "^2.0.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/ansi-escapes/node_modules/type-fest": { "version": "0.21.3", @@ -3866,6 +4362,13 @@ "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", "dev": true }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -5112,6 +5615,13 @@ "node": ">=0.8" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -6157,6 +6667,20 @@ "eslint": ">=5.0.0" } }, + "node_modules/eslint-plugin-typescript": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-typescript/-/eslint-plugin-typescript-0.14.0.tgz", + "integrity": "sha512-2u1WnnDF2mkWWgU1lFQ2RjypUlmRoBEvQN02y9u+IL12mjWlkKFGEBnVsjs9Y8190bfPQCvWly1c2rYYUSOxWw==", + "deprecated": "Deprecated: Use @typescript-eslint/eslint-plugin instead", + "dev": true, + "license": "MIT", + "dependencies": { + "requireindex": "~1.1.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -6610,6 +7134,36 @@ "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", "dev": true }, + "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==", + "dev": true, + "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-glob/node_modules/glob-parent": { + "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" + }, + "engines": { + "node": ">= 6" + } + }, "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", @@ -7113,6 +7667,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-tsconfig": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.0.tgz", + "integrity": "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==", + "dev": true, + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/getos": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/getos/-/getos-3.2.1.tgz", @@ -7271,7 +7837,14 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gopd": { + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "dev": true, + "license": "MIT" + }, + "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", @@ -7579,9 +8152,10 @@ ] }, "node_modules/ignore": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", - "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "license": "MIT", "engines": { "node": ">= 4" } @@ -9229,6 +9803,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -9297,6 +9878,16 @@ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "dev": true }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -9305,6 +9896,20 @@ "node": ">= 0.6" } }, + "node_modules/micromatch": { + "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", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -9649,6 +10254,25 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/nanoid": { + "version": "3.3.9", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.9.tgz", + "integrity": "sha512-SppoicMGpZvbF1l3z4x7No3OlIjP7QJvC9XR7AhZr1kL133KHnKPztkKDc+Ir4aJ/1VhTySrtKhrsycmrMQfvg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -10595,24 +11219,6 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/postcss/node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, "node_modules/precond": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz", @@ -11059,6 +11665,16 @@ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", "dev": true }, + "node_modules/requireindex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.1.0.tgz", + "integrity": "sha512-LBnkqsDE7BZKvqylbmn7lTIVdpx4K/QCduRATpO5R+wtPmky/a8pN1bO2D6wXppn1497AJF9mNjqAXr6bdl9jg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.5" + } + }, "node_modules/resolve": { "version": "2.0.0-next.5", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", @@ -11086,6 +11702,15 @@ "node": ">=8" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/restore-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", @@ -12268,6 +12893,117 @@ "tree-kill": "cli.js" } }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-mocha": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/ts-mocha/-/ts-mocha-11.1.0.tgz", + "integrity": "sha512-yT7FfzNRCu8ZKkYvAOiH01xNma/vLq6Vit7yINKYFNVP8e5UyrYXSOMIipERTpzVKJQ4Qcos5bQo1tNERNZevQ==", + "dev": true, + "license": "MIT", + "bin": { + "ts-mocha": "bin/ts-mocha" + }, + "engines": { + "node": ">= 6.X.X" + }, + "peerDependencies": { + "mocha": "^3.X.X || ^4.X.X || ^5.X.X || ^6.X.X || ^7.X.X || ^8.X.X || ^9.X.X || ^10.X.X || ^11.X.X", + "ts-node": "^7.X.X || ^8.X.X || ^9.X.X || ^10.X.X", + "tsconfig-paths": "^4.X.X" + }, + "peerDependenciesMeta": { + "tsconfig-paths": { + "optional": true + } + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/tsconfck": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.5.tgz", + "integrity": "sha512-CLDfGgUp7XPswWnezWwsCRxNmgQjhYq3VXHM0/XIRxhVrKw0M1if9agzryh1QS3nxjCROvV+xWxoJO1YctzzWg==", + "dev": true, + "license": "MIT", + "bin": { + "tsconfck": "bin/tsconfck.js" + }, + "engines": { + "node": "^18 || >=20" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", @@ -12281,6 +13017,417 @@ "node": ">=0.6.x" } }, + "node_modules/tsx": { + "version": "4.19.3", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.3.tgz", + "integrity": "sha512-4H8vUNGNjQ4V2EOoGw005+c+dGuPSnhpPBPHBtsZdGZBk/iJb4kguGlPWaZTZ3q5nMtFOEsY0nRDlh9PJyd6SQ==", + "dev": true, + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.1.tgz", + "integrity": "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.1.tgz", + "integrity": "sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.1.tgz", + "integrity": "sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.1.tgz", + "integrity": "sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.1.tgz", + "integrity": "sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.1.tgz", + "integrity": "sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.1.tgz", + "integrity": "sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.1.tgz", + "integrity": "sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.1.tgz", + "integrity": "sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ia32": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.1.tgz", + "integrity": "sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-loong64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.1.tgz", + "integrity": "sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.1.tgz", + "integrity": "sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.1.tgz", + "integrity": "sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.1.tgz", + "integrity": "sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-s390x": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.1.tgz", + "integrity": "sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.1.tgz", + "integrity": "sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.1.tgz", + "integrity": "sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.1.tgz", + "integrity": "sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/sunos-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.1.tgz", + "integrity": "sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.1.tgz", + "integrity": "sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-ia32": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.1.tgz", + "integrity": "sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.1.tgz", + "integrity": "sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/esbuild": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.1.tgz", + "integrity": "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.1", + "@esbuild/android-arm": "0.25.1", + "@esbuild/android-arm64": "0.25.1", + "@esbuild/android-x64": "0.25.1", + "@esbuild/darwin-arm64": "0.25.1", + "@esbuild/darwin-x64": "0.25.1", + "@esbuild/freebsd-arm64": "0.25.1", + "@esbuild/freebsd-x64": "0.25.1", + "@esbuild/linux-arm": "0.25.1", + "@esbuild/linux-arm64": "0.25.1", + "@esbuild/linux-ia32": "0.25.1", + "@esbuild/linux-loong64": "0.25.1", + "@esbuild/linux-mips64el": "0.25.1", + "@esbuild/linux-ppc64": "0.25.1", + "@esbuild/linux-riscv64": "0.25.1", + "@esbuild/linux-s390x": "0.25.1", + "@esbuild/linux-x64": "0.25.1", + "@esbuild/netbsd-arm64": "0.25.1", + "@esbuild/netbsd-x64": "0.25.1", + "@esbuild/openbsd-arm64": "0.25.1", + "@esbuild/openbsd-x64": "0.25.1", + "@esbuild/sunos-x64": "0.25.1", + "@esbuild/win32-arm64": "0.25.1", + "@esbuild/win32-ia32": "0.25.1", + "@esbuild/win32-x64": "0.25.1" + } + }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -12434,12 +13581,11 @@ } }, "node_modules/typescript": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", - "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -12479,9 +13625,10 @@ } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "license": "MIT" }, "node_modules/unicorn-magic": { "version": "0.1.0", @@ -12600,6 +13747,13 @@ "uuid": "dist/esm/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", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -12701,6 +13855,26 @@ } } }, + "node_modules/vite-tsconfig-paths": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz", + "integrity": "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "globrex": "^0.1.2", + "tsconfck": "^3.0.3" + }, + "peerDependencies": { + "vite": "*" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, "node_modules/vscode-json-languageservice": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/vscode-json-languageservice/-/vscode-json-languageservice-4.2.1.tgz", @@ -13107,6 +14281,16 @@ "fd-slicer": "~1.1.0" } }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index d2f1086a9..757dfbd92 100644 --- a/package.json +++ b/package.json @@ -6,10 +6,11 @@ "cli": "node ./packages/git-proxy-cli/index.js", "client": "vite --config vite.config.js", "clientinstall": "npm install --prefix client", - "server": "node index.js", + "server": "tsx index.ts", "start": "concurrently \"npm run server\" \"npm run client\"", "build": "vite build", - "test": "NODE_ENV=test mocha './test/**/*.js' --exit", + "build-ts": "tsc", + "test": "NODE_ENV=test ts-mocha './test/*.js' --exit", "test-coverage": "nyc npm run test", "test-coverage-ci": "nyc --reporter=lcovonly --reporter=text npm run test", "prepare": "node ./scripts/prepare.js", @@ -80,6 +81,14 @@ "@babel/preset-react": "^7.22.5", "@commitlint/cli": "^19.0.0", "@commitlint/config-conventional": "^19.0.0", + "@types/express": "^5.0.1", + "@types/express-http-proxy": "^1.6.6", + "@types/lodash": "^4.17.15", + "@types/mocha": "^10.0.10", + "@types/node": "^22.13.5", + "@types/yargs": "^17.0.33", + "@typescript-eslint/eslint-plugin": "^8.26.1", + "@typescript-eslint/parser": "^8.26.1", "@vitejs/plugin-react": "^4.0.2", "chai": "^4.2.0", "chai-http": "^4.3.0", @@ -91,12 +100,18 @@ "eslint-plugin-prettier": "^5.0.0", "eslint-plugin-react": "^7.21.5", "eslint-plugin-standard": "^5.0.0", + "eslint-plugin-typescript": "^0.14.0", "husky": "^9.0.0", - "mocha": "^10.2.0", + "mocha": "^10.8.2", "nyc": "^17.0.0", "prettier": "^3.0.0", "sinon": "^19.0.2", - "vite": "^4.4.2" + "ts-mocha": "^11.1.0", + "ts-node": "^10.9.2", + "tsx": "^4.19.3", + "typescript": "^5.7.3", + "vite": "4.5.5", + "vite-tsconfig-paths": "^5.1.4" }, "optionalDependencies": { "@esbuild/darwin-arm64": "^0.18.20", diff --git a/packages/git-proxy-cli/index.js b/packages/git-proxy-cli/index.js index e3c69ce8d..b0090a4bf 100755 --- a/packages/git-proxy-cli/index.js +++ b/packages/git-proxy-cli/index.js @@ -7,8 +7,8 @@ const util = require('util'); const GIT_PROXY_COOKIE_FILE = 'git-proxy-cookie'; // GitProxy UI HOST and PORT (configurable via environment variable) -const { GIT_PROXY_UI_HOST: uiHost = 'http://localhost' } = process.env; -const { GIT_PROXY_UI_PORT: uiPort } = require('@finos/git-proxy/src/config/env').Vars; +const { GIT_PROXY_UI_HOST: uiHost = 'http://localhost', GIT_PROXY_UI_PORT: uiPort = 8080 } = process.env; + const baseUrl = `${uiHost}:${uiPort}`; axios.defaults.timeout = 30000; @@ -307,7 +307,7 @@ async function logout() { } // Parsing command line arguments -yargs(hideBin(process.argv)) +yargs(hideBin(process.argv)) // eslint-disable-line @typescript-eslint/no-unused-expressions .command({ command: 'authorise', describe: 'Authorise git push by ID', @@ -339,7 +339,7 @@ yargs(hideBin(process.argv)) .command({ command: 'config', describe: 'Print configuration', - handler(argv) { + handler() { console.log(`GitProxy URL: ${baseUrl}`); }, }) @@ -365,7 +365,7 @@ yargs(hideBin(process.argv)) .command({ command: 'logout', describe: 'Log out', - handler(argv) { + handler() { logout(); }, }) diff --git a/packages/git-proxy-cli/package.json b/packages/git-proxy-cli/package.json index c2bcf6ef4..baade725c 100644 --- a/packages/git-proxy-cli/package.json +++ b/packages/git-proxy-cli/package.json @@ -13,7 +13,7 @@ }, "scripts": { "lint": "eslint --fix . --ext .js,.jsx", - "test": "NODE_ENV=test mocha --exit --timeout 10000", + "test": "NODE_ENV=test ts-mocha --exit --timeout 10000", "test-coverage": "nyc npm run test", "test-coverage-ci": "nyc --reporter=lcovonly --reporter=text --reporter=html npm run test" }, diff --git a/packages/git-proxy-cli/tsconfig.json b/packages/git-proxy-cli/tsconfig.json new file mode 100644 index 000000000..236bfabc5 --- /dev/null +++ b/packages/git-proxy-cli/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES6", + "lib": ["DOM", "ESNext"], + "allowJs": true, + "checkJs": false, + "jsx": "react-jsx", + "moduleResolution": "Node", + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "isolatedModules": true, + "module": "CommonJS", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true + }, + "include": ["index.js", "test", "coverage"] +} diff --git a/src/config/env.js b/src/config/env.js deleted file mode 100644 index 709f8bbb4..000000000 --- a/src/config/env.js +++ /dev/null @@ -1,3 +0,0 @@ -const { GIT_PROXY_SERVER_PORT = 8000, GIT_PROXY_HTTPS_SERVER_PORT = 8443, GIT_PROXY_UI_PORT = 8080 } = process.env; - -exports.Vars = { GIT_PROXY_SERVER_PORT, GIT_PROXY_HTTPS_SERVER_PORT, GIT_PROXY_UI_PORT }; diff --git a/src/config/env.ts b/src/config/env.ts new file mode 100644 index 000000000..85b8475b5 --- /dev/null +++ b/src/config/env.ts @@ -0,0 +1,20 @@ +export type ServerConfig = { + GIT_PROXY_SERVER_PORT: string | number; + GIT_PROXY_HTTPS_SERVER_PORT: string | number; + GIT_PROXY_UI_HOST: string; + GIT_PROXY_UI_PORT: string | number; +}; + +const { + GIT_PROXY_SERVER_PORT = 8000, + GIT_PROXY_HTTPS_SERVER_PORT = 8443, + GIT_PROXY_UI_HOST = 'http://localhost', + GIT_PROXY_UI_PORT = 8080 +} = process.env; + +export const serverConfig: ServerConfig = { + GIT_PROXY_SERVER_PORT, + GIT_PROXY_HTTPS_SERVER_PORT, + GIT_PROXY_UI_HOST, + GIT_PROXY_UI_PORT +}; diff --git a/src/config/file.js b/src/config/file.js deleted file mode 100644 index 1cc99abb1..000000000 --- a/src/config/file.js +++ /dev/null @@ -1,32 +0,0 @@ -const path = require('path'); -// eslint-disable-next-line prefer-const -let configFile = undefined; - -/** - * Validate config file. - * @param {string} configFilePath - The path to the config file. - * @return {boolean} - Returns true if validation is successful. - * @throws Will throw an error if the validation fails. - */ -function validate(configFilePath = configFile) { - const fs = require('fs'); - const path = require('path'); - const validate = require('jsonschema').validate; - - const config = JSON.parse(fs.readFileSync(configFilePath)); - const schema = JSON.parse( - fs.readFileSync(path.join(__dirname, '..', '..', 'config.schema.json')), - ); - validate(config, schema, { required: true, throwError: true }); - return true; -} - -module.exports = { - get configFile() { - return configFile ? configFile : path.join(process.cwd(), 'proxy.config.json'); - }, - set configFile(file) { - configFile = file; - }, - validate, -}; diff --git a/src/config/file.ts b/src/config/file.ts new file mode 100644 index 000000000..e7aadcd46 --- /dev/null +++ b/src/config/file.ts @@ -0,0 +1,27 @@ +import { readFileSync } from 'fs'; +import { join } from 'path'; +import { validate as jsonSchemaValidate } from 'jsonschema'; + +export let configFile: string = join(process.cwd(), 'proxy.config.json'); + +/** + * Set the config file path. + * @param {string} file - The path to the config file. + */ +export function setConfigFile(file: string) { + configFile = file; +} + +/** + * Validate config file. + * @param {string} configFilePath - The path to the config file. + * @return {boolean} - Returns true if validation is successful. + * @throws Will throw an error if the validation fails. + */ +export function validate(configFilePath: string = configFile!): boolean { + const config = JSON.parse(readFileSync(configFilePath, 'utf-8')); + const schemaPath = join(process.cwd(), 'config.schema.json'); + const schema = JSON.parse(readFileSync(schemaPath, 'utf-8')); + jsonSchemaValidate(config, schema, { required: true, throwError: true }); + return true; +} diff --git a/src/config/index.js b/src/config/index.ts similarity index 57% rename from src/config/index.js rename to src/config/index.ts index 78184d413..a1779ed9d 100644 --- a/src/config/index.js +++ b/src/config/index.ts @@ -1,33 +1,36 @@ -const fs = require('fs'); +import { existsSync, readFileSync } from 'fs'; -const defaultSettings = require('../../proxy.config.json'); -const userSettingsPath = require('./file').configFile; +import defaultSettings from '../../proxy.config.json'; +import { configFile } from './file'; +import { Authentication, AuthorisedRepo, Database, TempPasswordConfig, UserSettings } from './types'; -let _userSettings = null; -if (fs.existsSync(userSettingsPath)) { - _userSettings = JSON.parse(fs.readFileSync(userSettingsPath)); + +let _userSettings: UserSettings | null = null; +if (existsSync(configFile)) { + _userSettings = JSON.parse(readFileSync(configFile, 'utf-8')); } -let _authorisedList = defaultSettings.authorisedList; -let _database = defaultSettings.sink; -let _authentication = defaultSettings.authentication; -let _tempPassword = defaultSettings.tempPassword; +let _authorisedList: AuthorisedRepo[] = defaultSettings.authorisedList; +let _database: Database[] = defaultSettings.sink; +let _authentication: Authentication[] = defaultSettings.authentication; +let _tempPassword: TempPasswordConfig = defaultSettings.tempPassword; let _proxyUrl = defaultSettings.proxyUrl; -let _api = defaultSettings.api; -let _cookieSecret = defaultSettings.cookieSecret; -let _sessionMaxAgeHours = defaultSettings.sessionMaxAgeHours; -let _sslKeyPath = defaultSettings.sslKeyPemPath; -let _sslCertPath = defaultSettings.sslCertPemPath; -let _plugins = defaultSettings.plugins; -let _commitConfig = defaultSettings.commitConfig; -let _attestationConfig = defaultSettings.attestationConfig; -let _privateOrganizations = defaultSettings.privateOrganizations; -let _urlShortener = defaultSettings.urlShortener; -let _contactEmail = defaultSettings.contactEmail; -let _csrfProtection = defaultSettings.csrfProtection; -let _domains = defaultSettings.domains; +let _api: Record = defaultSettings.api; +let _cookieSecret: string = defaultSettings.cookieSecret; +let _sessionMaxAgeHours: number = defaultSettings.sessionMaxAgeHours; +let _plugins: any[] = defaultSettings.plugins; +let _commitConfig: Record = defaultSettings.commitConfig; +let _attestationConfig: Record = defaultSettings.attestationConfig; +let _privateOrganizations: string[] = defaultSettings.privateOrganizations; +let _urlShortener: string = defaultSettings.urlShortener; +let _contactEmail: string = defaultSettings.contactEmail; +let _csrfProtection: boolean = defaultSettings.csrfProtection; +let _domains: Record = defaultSettings.domains; +// These are not always present in the default config file, so casting is required +let _sslKeyPath: string = (defaultSettings as any).sslKeyPemPath; +let _sslCertPath: string = (defaultSettings as any).sslCertPemPath; // Get configured proxy URL -const getProxyUrl = () => { +export const getProxyUrl = () => { if (_userSettings !== null && _userSettings.proxyUrl) { _proxyUrl = _userSettings.proxyUrl; } @@ -36,16 +39,15 @@ const getProxyUrl = () => { }; // Gets a list of authorised repositories -const getAuthorisedList = () => { +export const getAuthorisedList = () => { if (_userSettings !== null && _userSettings.authorisedList) { _authorisedList = _userSettings.authorisedList; } - return _authorisedList; }; // Gets a list of authorised repositories -const getTempPasswordConfig = () => { +export const getTempPasswordConfig = () => { if (_userSettings !== null && _userSettings.tempPassword) { _tempPassword = _userSettings.tempPassword; } @@ -53,8 +55,8 @@ const getTempPasswordConfig = () => { return _tempPassword; }; -// Gets the configuared data sink, defaults to filesystem -const getDatabase = () => { +// Gets the configured data sink, defaults to filesystem +export const getDatabase = () => { if (_userSettings !== null && _userSettings.sink) { _database = _userSettings.sink; } @@ -70,8 +72,8 @@ const getDatabase = () => { throw Error('No database cofigured!'); }; -// Gets the configuared data sink, defaults to filesystem -const getAuthentication = () => { +// Gets the configured authentication method, defaults to local +export const getAuthentication = () => { if (_userSettings !== null && _userSettings.authentication) { _authentication = _userSettings.authentication; } @@ -87,27 +89,27 @@ const getAuthentication = () => { }; // Log configuration to console -const logConfiguration = () => { +export const logConfiguration = () => { console.log(`authorisedList = ${JSON.stringify(getAuthorisedList())}`); console.log(`data sink = ${JSON.stringify(getDatabase())}`); console.log(`authentication = ${JSON.stringify(getAuthentication())}`); }; -const getAPIs = () => { +export const getAPIs = () => { if (_userSettings && _userSettings.api) { _api = _userSettings.api; } return _api; }; -const getCookieSecret = () => { +export const getCookieSecret = () => { if (_userSettings && _userSettings.cookieSecret) { _cookieSecret = _userSettings.cookieSecret; } return _cookieSecret; }; -const getSessionMaxAgeHours = () => { +export const getSessionMaxAgeHours = () => { if (_userSettings && _userSettings.sessionMaxAgeHours) { _sessionMaxAgeHours = _userSettings.sessionMaxAgeHours; } @@ -115,7 +117,7 @@ const getSessionMaxAgeHours = () => { }; // Get commit related configuration -const getCommitConfig = () => { +export const getCommitConfig = () => { if (_userSettings && _userSettings.commitConfig) { _commitConfig = _userSettings.commitConfig; } @@ -123,7 +125,7 @@ const getCommitConfig = () => { }; // Get attestation related configuration -const getAttestationConfig = () => { +export const getAttestationConfig = () => { if (_userSettings && _userSettings.attestationConfig) { _attestationConfig = _userSettings.attestationConfig; } @@ -131,7 +133,7 @@ const getAttestationConfig = () => { }; // Get private organizations related configuration -const getPrivateOrganizations = () => { +export const getPrivateOrganizations = () => { if (_userSettings && _userSettings.privateOrganizations) { _privateOrganizations = _userSettings.privateOrganizations; } @@ -139,7 +141,7 @@ const getPrivateOrganizations = () => { }; // Get URL shortener -const getURLShortener = () => { +export const getURLShortener = () => { if (_userSettings && _userSettings.urlShortener) { _urlShortener = _userSettings.urlShortener; } @@ -147,7 +149,7 @@ const getURLShortener = () => { }; // Get contact e-mail address -const getContactEmail = () => { +export const getContactEmail = () => { if (_userSettings && _userSettings.contactEmail) { _contactEmail = _userSettings.contactEmail; } @@ -155,7 +157,7 @@ const getContactEmail = () => { }; // Get CSRF protection flag -const getCSRFProtection = () => { +export const getCSRFProtection = () => { if (_userSettings && _userSettings.csrfProtection) { _csrfProtection = _userSettings.csrfProtection; } @@ -163,14 +165,14 @@ const getCSRFProtection = () => { }; // Get loadable push plugins -const getPlugins = () => { +export const getPlugins = () => { if (_userSettings && _userSettings.plugins) { _plugins = _userSettings.plugins; } return _plugins; } -const getSSLKeyPath = () => { +export const getSSLKeyPath = () => { if (_userSettings && _userSettings.sslKeyPemPath) { _sslKeyPath = _userSettings.sslKeyPemPath; } @@ -180,7 +182,7 @@ const getSSLKeyPath = () => { return _sslKeyPath; }; -const getSSLCertPath = () => { +export const getSSLCertPath = () => { if (_userSettings && _userSettings.sslCertPemPath) { _sslCertPath = _userSettings.sslCertPemPath; } @@ -190,29 +192,9 @@ const getSSLCertPath = () => { return _sslCertPath; }; -const getDomains = () => { +export const getDomains = () => { if (_userSettings && _userSettings.domains) { _domains = _userSettings.domains; } return _domains; }; - -exports.getAPIs = getAPIs; -exports.getProxyUrl = getProxyUrl; -exports.getAuthorisedList = getAuthorisedList; -exports.getDatabase = getDatabase; -exports.logConfiguration = logConfiguration; -exports.getAuthentication = getAuthentication; -exports.getTempPasswordConfig = getTempPasswordConfig; -exports.getCookieSecret = getCookieSecret; -exports.getSessionMaxAgeHours = getSessionMaxAgeHours; -exports.getCommitConfig = getCommitConfig; -exports.getAttestationConfig = getAttestationConfig; -exports.getPrivateOrganizations = getPrivateOrganizations; -exports.getURLShortener = getURLShortener; -exports.getContactEmail = getContactEmail; -exports.getCSRFProtection = getCSRFProtection; -exports.getPlugins = getPlugins; -exports.getSSLKeyPath = getSSLKeyPath; -exports.getSSLCertPath = getSSLCertPath; -exports.getDomains = getDomains; diff --git a/src/config/types.ts b/src/config/types.ts new file mode 100644 index 000000000..7364f6201 --- /dev/null +++ b/src/config/types.ts @@ -0,0 +1,45 @@ +export interface UserSettings { + authorisedList: AuthorisedRepo[]; + sink: Database[]; + authentication: Authentication[]; + tempPassword?: TempPasswordConfig; + proxyUrl: string; + api: Record; + cookieSecret: string; + sessionMaxAgeHours: number; + sslKeyPemPath?: string; // Optional (not in config.schema.json) + sslCertPemPath?: string; // Optional (not in config.schema.json) + plugins: any[]; + commitConfig: Record; + attestationConfig: Record; + privateOrganizations: any[]; + urlShortener: string; + contactEmail: string; + csrfProtection: boolean; + domains: Record; +} + +export interface AuthorisedRepo { + project: string; + name: string; + url: string; +} + +export interface Database { + type: string; + enabled: boolean; + connectionString?: string; + params?: Record; + options?: Record; +} + +export interface Authentication { + type: string; + enabled: boolean; + options?: Record; +} + +export interface TempPasswordConfig { + sendEmail: boolean; + emailConfig: Record; +} diff --git a/src/db/file/index.js b/src/db/file/index.js deleted file mode 100644 index 03dd8ecf0..000000000 --- a/src/db/file/index.js +++ /dev/null @@ -1,30 +0,0 @@ -const pushes = require('./pushes'); -const users = require('./users'); -const repo = require('./repo'); - -module.exports.getPushes = pushes.getPushes; -module.exports.writeAudit = pushes.writeAudit; -module.exports.getPush = pushes.getPush; -module.exports.authorise = pushes.authorise; -module.exports.cancel = pushes.cancel; -module.exports.reject = pushes.reject; -module.exports.canUserCancelPush = pushes.canUserCancelPush; -module.exports.canUserApproveRejectPush = pushes.canUserApproveRejectPush; - -module.exports.findUser = users.findUser; -module.exports.findUserByOIDC = users.findUserByOIDC; -module.exports.getUsers = users.getUsers; -module.exports.createUser = users.createUser; -module.exports.deleteUser = users.deleteUser; -module.exports.updateUser = users.updateUser; - -module.exports.getRepos = repo.getRepos; -module.exports.getRepo = repo.getRepo; -module.exports.createRepo = repo.createRepo; -module.exports.addUserCanPush = repo.addUserCanPush; -module.exports.addUserCanAuthorise = repo.addUserCanAuthorise; -module.exports.removeUserCanPush = repo.removeUserCanPush; -module.exports.removeUserCanAuthorise = repo.removeUserCanAuthorise; -module.exports.deleteRepo = repo.deleteRepo; -module.exports.isUserPushAllowed = repo.isUserPushAllowed; -module.exports.canUserApproveRejectPushRepo = repo.canUserApproveRejectPushRepo; diff --git a/src/db/file/index.ts b/src/db/file/index.ts new file mode 100644 index 000000000..a31610173 --- /dev/null +++ b/src/db/file/index.ts @@ -0,0 +1,36 @@ +import * as users from './users'; +import * as repo from './repo'; +import * as pushes from './pushes'; + +export const { + getPushes, + writeAudit, + getPush, + authorise, + cancel, + reject, + canUserCancelPush, + canUserApproveRejectPush, +} = pushes; + +export const { + getRepos, + getRepo, + createRepo, + addUserCanPush, + addUserCanAuthorise, + removeUserCanPush, + removeUserCanAuthorise, + deleteRepo, + isUserPushAllowed, + canUserApproveRejectPushRepo, +} = repo; + +export const { + findUser, + findUserByOIDC, + getUsers, + createUser, + deleteUser, + updateUser, +} = users; diff --git a/src/db/file/pushes.js b/src/db/file/pushes.ts similarity index 59% rename from src/db/file/pushes.js rename to src/db/file/pushes.ts index cbc79244a..9901940f7 100644 --- a/src/db/file/pushes.js +++ b/src/db/file/pushes.ts @@ -1,26 +1,27 @@ -const fs = require('fs'); -const _ = require('lodash'); -const Datastore = require('@seald-io/nedb'); -const Action = require('../../proxy/actions/Action').Action; -const toClass = require('../helper').toClass; -const repo = require('./repo'); +import fs from 'fs'; +import _ from 'lodash'; +import Datastore from '@seald-io/nedb'; +import { Action } from '../../proxy/actions/Action'; +import { toClass } from '../helper'; +import * as repo from './repo'; +import { PushQuery } from '../types' if (!fs.existsSync('./.data')) fs.mkdirSync('./.data'); if (!fs.existsSync('./.data/db')) fs.mkdirSync('./.data/db'); const db = new Datastore({ filename: './.data/db/pushes.db', autoload: true }); -const defaultPushQuery = { +const defaultPushQuery: PushQuery = { error: false, blocked: true, allowPush: false, authorised: false, }; -const getPushes = (query, logger) => { +export const getPushes = (query: PushQuery) => { if (!query) query = defaultPushQuery; return new Promise((resolve, reject) => { - db.find(query, (err, docs) => { + db.find(query, (err: Error, docs: Action[]) => { if (err) { reject(err); } else { @@ -34,8 +35,8 @@ const getPushes = (query, logger) => { }); }; -const getPush = async (id, logger) => { - return new Promise((resolve, reject) => { +export const getPush = async (id: string) => { + return new Promise((resolve, reject) => { db.findOne({ id: id }, (err, doc) => { if (err) { reject(err); @@ -50,7 +51,7 @@ const getPush = async (id, logger) => { }); }; -const writeAudit = async (action, logger) => { +export const writeAudit = async (action: Action) => { return new Promise((resolve, reject) => { const options = { multi: false, upsert: true }; db.update({ id: action.id }, action, options, (err) => { @@ -63,8 +64,12 @@ const writeAudit = async (action, logger) => { }); }; -const authorise = async (id, attestation) => { +export const authorise = async (id: string, attestation: any) => { const action = await getPush(id); + if (!action) { + throw new Error(`push ${id} not found`); + } + action.authorised = true; action.canceled = false; action.rejected = false; @@ -73,8 +78,12 @@ const authorise = async (id, attestation) => { return { message: `authorised ${id}` }; }; -const reject = async (id, logger) => { - const action = await getPush(id, logger); +export const reject = async (id: string) => { + const action = await getPush(id); + if (!action) { + throw new Error(`push ${id} not found`); + } + action.authorised = false; action.canceled = false; action.rejected = true; @@ -82,8 +91,11 @@ const reject = async (id, logger) => { return { message: `reject ${id}` }; }; -const cancel = async (id, logger) => { - const action = await getPush(id, logger); +export const cancel = async (id: string) => { + const action = await getPush(id); + if (!action) { + throw new Error(`push ${id} not found`); + } action.authorised = false; action.canceled = true; action.rejected = false; @@ -91,9 +103,14 @@ const cancel = async (id, logger) => { return { message: `cancel ${id}` }; }; -const canUserCancelPush = async (id, user) => { - return new Promise(async (resolve) => { +export const canUserCancelPush = async (id: string, user: any) => { + return new Promise(async (resolve) => { const pushDetail = await getPush(id); + if (!pushDetail) { + resolve(false); + return; + } + const repoName = pushDetail.repoName.replace('.git', ''); const isAllowed = await repo.isUserPushAllowed(repoName, user); @@ -105,21 +122,16 @@ const canUserCancelPush = async (id, user) => { }); }; -const canUserApproveRejectPush = async (id, user) => { - return new Promise(async (resolve) => { +export const canUserApproveRejectPush = async (id: string, user: any) => { + return new Promise(async (resolve) => { const action = await getPush(id); - const repoName = action.repoName.replace('.git', ''); + if (!action) { + resolve(false); + return; + } + const repoName = action?.repoName.replace('.git', ''); const isAllowed = await repo.canUserApproveRejectPushRepo(repoName, user); resolve(isAllowed); }); }; - -module.exports.getPushes = getPushes; -module.exports.writeAudit = writeAudit; -module.exports.getPush = getPush; -module.exports.authorise = authorise; -module.exports.reject = reject; -module.exports.cancel = cancel; -module.exports.canUserCancelPush = canUserCancelPush; -module.exports.canUserApproveRejectPush = canUserApproveRejectPush; diff --git a/src/db/file/repo.js b/src/db/file/repo.ts similarity index 58% rename from src/db/file/repo.js rename to src/db/file/repo.ts index e6f7dd894..8686899f5 100644 --- a/src/db/file/repo.js +++ b/src/db/file/repo.ts @@ -1,14 +1,15 @@ -const fs = require('fs'); -const Datastore = require('@seald-io/nedb'); +import fs from 'fs'; +import Datastore from '@seald-io/nedb' +import { Repo } from '../types'; if (!fs.existsSync('./.data')) fs.mkdirSync('./.data'); if (!fs.existsSync('./.data/db')) fs.mkdirSync('./.data/db'); const db = new Datastore({ filename: './.data/db/repos.db', autoload: true }); -exports.getRepos = async (query = {}) => { - return new Promise((resolve, reject) => { - db.find({}, (err, docs) => { +export const getRepos = async (query: any = {}) => { + return new Promise((resolve, reject) => { + db.find({}, (err: Error, docs: Repo[]) => { if (err) { reject(err); } else { @@ -18,29 +19,26 @@ exports.getRepos = async (query = {}) => { }); }; -exports.getRepo = async (name) => { - return new Promise((resolve, reject) => { - db.findOne({ name: name }, (err, doc) => { +export const getRepo = async (name: string) => { + return new Promise((resolve, reject) => { + db.findOne({ name }, (err: Error | null, doc: Repo) => { if (err) { reject(err); } else { - if (!doc) { - resolve(null); - } else { - resolve(doc); - } + resolve(doc); } }); }); }; -exports.createRepo = async (repo) => { + +export const createRepo = async (repo: Repo) => { repo.users = { canPush: [], canAuthorise: [], }; - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { db.insert(repo, (err, doc) => { if (err) { reject(err); @@ -51,9 +49,13 @@ exports.createRepo = async (repo) => { }); }; -exports.addUserCanPush = async (name, user) => { +export const addUserCanPush = async (name: string, user: string) => { return new Promise(async (resolve, reject) => { - const repo = await exports.getRepo(name); + const repo = await getRepo(name); + if (!repo) { + reject(new Error('Repo not found')); + return; + } if (repo.users.canPush.includes(user)) { resolve(null); @@ -72,9 +74,13 @@ exports.addUserCanPush = async (name, user) => { }); }; -exports.addUserCanAuthorise = async (name, user) => { +export const addUserCanAuthorise = async (name: string, user: string) => { return new Promise(async (resolve, reject) => { - const repo = await exports.getRepo(name); + const repo = await getRepo(name); + if (!repo) { + reject(new Error('Repo not found')); + return; + } if (repo.users.canAuthorise.includes(user)) { resolve(null); @@ -94,11 +100,15 @@ exports.addUserCanAuthorise = async (name, user) => { }); }; -exports.removeUserCanAuthorise = async (name, user) => { +export const removeUserCanAuthorise = async (name: string, user: string) => { return new Promise(async (resolve, reject) => { - const repo = await exports.getRepo(name); + const repo = await getRepo(name); + if (!repo) { + reject(new Error('Repo not found')); + return; + } - repo.users.canAuthorise = repo.users.canAuthorise.filter((x) => x != user); + repo.users.canAuthorise = repo.users.canAuthorise.filter((x: string) => x != user); const options = { multi: false, upsert: false }; db.update({ name: name }, repo, options, (err) => { @@ -111,9 +121,13 @@ exports.removeUserCanAuthorise = async (name, user) => { }); }; -exports.removeUserCanPush = async (name, user) => { +export const removeUserCanPush = async (name: string, user: string) => { return new Promise(async (resolve, reject) => { - const repo = await exports.getRepo(name); + const repo = await getRepo(name); + if (!repo) { + reject(new Error('Repo not found')); + return; + } repo.users.canPush = repo.users.canPush.filter((x) => x != user); @@ -128,8 +142,8 @@ exports.removeUserCanPush = async (name, user) => { }); }; -exports.deleteRepo = async (name) => { - return new Promise((resolve, reject) => { +export const deleteRepo = async (name: string) => { + return new Promise((resolve, reject) => { db.remove({ name: name }, (err) => { if (err) { reject(err); @@ -140,10 +154,15 @@ exports.deleteRepo = async (name) => { }); }; -exports.isUserPushAllowed = async (name, user) => { +export const isUserPushAllowed = async (name: string, user: string) => { name = name.toLowerCase(); - return new Promise(async (resolve, reject) => { - const repo = await exports.getRepo(name); + return new Promise(async (resolve) => { + const repo = await getRepo(name); + if (!repo) { + resolve(false); + return; + } + console.log(repo.users.canPush); console.log(repo.users.canAuthorise); @@ -155,11 +174,16 @@ exports.isUserPushAllowed = async (name, user) => { }); }; -exports.canUserApproveRejectPushRepo = async (name, user) => { +export const canUserApproveRejectPushRepo = async (name: string, user: string) => { name = name.toLowerCase(); console.log(`checking if user ${user} can approve/reject for ${name}`); - return new Promise(async (resolve, reject) => { - const repo = await exports.getRepo(name); + return new Promise(async (resolve) => { + const repo = await getRepo(name); + if (!repo) { + resolve(false); + return; + } + if (repo.users.canAuthorise.includes(user)) { console.log(`user ${user} can approve/reject to repo ${name}`); resolve(true); diff --git a/src/db/file/users.js b/src/db/file/users.ts similarity index 63% rename from src/db/file/users.js rename to src/db/file/users.ts index 2661006af..d72443c97 100644 --- a/src/db/file/users.js +++ b/src/db/file/users.ts @@ -1,14 +1,15 @@ -const fs = require('fs'); -const Datastore = require('@seald-io/nedb'); +import fs from 'fs'; +import Datastore from '@seald-io/nedb'; +import { User } from '../types'; if (!fs.existsSync('./.data')) fs.mkdirSync('./.data'); if (!fs.existsSync('./.data/db')) fs.mkdirSync('./.data/db'); const db = new Datastore({ filename: './.data/db/users.db', autoload: true }); -exports.findUser = function (username) { - return new Promise((resolve, reject) => { - db.findOne({ username: username }, (err, doc) => { +export const findUser = (username: string) => { + return new Promise((resolve, reject) => { + db.findOne({ username: username }, (err: Error | null, doc: User) => { if (err) { reject(err); } else { @@ -22,7 +23,7 @@ exports.findUser = function (username) { }); }; -exports.findUserByOIDC = function (oidcId) { +export const findUserByOIDC = function (oidcId: string) { return new Promise((resolve, reject) => { db.findOne({ oidcId: oidcId }, (err, doc) => { if (err) { @@ -38,20 +39,20 @@ exports.findUserByOIDC = function (oidcId) { }); }; -exports.createUser = function (data) { +export const createUser = function (user: User) { return new Promise((resolve, reject) => { - db.insert(data, (err) => { + db.insert(user, (err) => { if (err) { reject(err); } else { - resolve(data); + resolve(user); } }); }); }; -exports.deleteUser = function (username) { - return new Promise((resolve, reject) => { +export const deleteUser = (username: string) => { + return new Promise((resolve, reject) => { db.remove({ username: username }, (err) => { if (err) { reject(err); @@ -62,7 +63,7 @@ exports.deleteUser = function (username) { }); }; -exports.updateUser = function (user) { +export const updateUser = (user: User) => { return new Promise((resolve, reject) => { const options = { multi: false, upsert: false }; db.update({ username: user.username }, user, options, (err) => { @@ -75,10 +76,9 @@ exports.updateUser = function (user) { }); }; -exports.getUsers = function (query) { - if (!query) query = {}; - return new Promise((resolve, reject) => { - db.find(query, (err, docs) => { +export const getUsers = (query: any = {}) => { + return new Promise((resolve, reject) => { + db.find(query, (err: Error, docs: User[]) => { if (err) { reject(err); } else { diff --git a/src/db/helper.js b/src/db/helper.ts similarity index 52% rename from src/db/helper.js rename to src/db/helper.ts index 8faa636e7..55c5aca0b 100644 --- a/src/db/helper.js +++ b/src/db/helper.ts @@ -1,7 +1,5 @@ -const toClass = function (obj, proto) { +export const toClass = function (obj: any, proto: any) { obj = JSON.parse(JSON.stringify(obj)); obj.__proto__ = proto; return obj; }; - -module.exports.toClass = toClass; diff --git a/src/db/index.js b/src/db/index.js deleted file mode 100644 index ed2c13524..000000000 --- a/src/db/index.js +++ /dev/null @@ -1,85 +0,0 @@ -const bcrypt = require('bcryptjs'); -const config = require('../config'); -let sink; -if (config.getDatabase().type === 'mongo') { - sink = require('../db/mongo'); -} else if (config.getDatabase().type === 'fs') { - sink = require('../db/file'); -} - -module.exports.createUser = async ( - username, - password, - email, - gitAccount, - admin = false, - oidcId = null, -) => { - console.log( - `creating user - user=${username}, - gitAccount=${gitAccount} - email=${email}, - admin=${admin} - oidcId=${oidcId}`, - ); - - const data = { - username: username, - password: oidcId ? null : await bcrypt.hash(password, 10), - gitAccount: gitAccount, - email: email, - admin: admin, - oidcId: oidcId, - }; - - if (username === undefined || username === null || username === '') { - const errorMessage = `username ${username} cannot be empty`; - throw new Error(errorMessage); - } - - if (gitAccount === undefined || gitAccount === null || gitAccount === '') { - const errorMessage = `GitAccount ${gitAccount} cannot be empty`; - throw new Error(errorMessage); - } - - if (email === undefined || email === null || email === '') { - const errorMessage = `Email ${email} cannot be empty`; - throw new Error(errorMessage); - } - const existingUser = await sink.findUser(username); - - if (existingUser) { - const errorMessage = `user ${username} already exists`; - throw new Error(errorMessage); - } - - await sink.createUser(data); -}; - -// The module exports -module.exports.authorise = sink.authorise; -module.exports.reject = sink.reject; -module.exports.cancel = sink.cancel; -module.exports.getPushes = sink.getPushes; -module.exports.writeAudit = sink.writeAudit; -module.exports.getPush = sink.getPush; -module.exports.findUser = sink.findUser; -module.exports.findUserByOIDC = sink.findUserByOIDC; -module.exports.getUsers = sink.getUsers; -module.exports.deleteUser = sink.deleteUser; -module.exports.updateUser = sink.updateUser; -module.exports.getRepos = sink.getRepos; -module.exports.getRepo = sink.getRepo; -module.exports.createRepo = sink.createRepo; -module.exports.addUserCanPush = sink.addUserCanPush; -module.exports.addUserCanAuthorise = sink.addUserCanAuthorise; -module.exports.removeUserCanAuthorise = sink.removeUserCanAuthorise; -module.exports.removeUserCanPush = sink.removeUserCanPush; - -module.exports.deleteRepo = sink.deleteRepo; -module.exports.isUserPushAllowed = sink.isUserPushAllowed; -module.exports.canUserApproveRejectPushRepo = sink.canUserApproveRejectPushRepo; -module.exports.canUserApproveRejectPush = sink.canUserApproveRejectPush; -module.exports.canUserCancelPush = sink.canUserCancelPush; -module.exports.getSessionStore = sink.getSessionStore; diff --git a/src/db/index.ts b/src/db/index.ts new file mode 100644 index 000000000..0fc681058 --- /dev/null +++ b/src/db/index.ts @@ -0,0 +1,84 @@ +const bcrypt = require('bcryptjs'); +const config = require('../config'); +let sink: any; +if (config.getDatabase().type === 'mongo') { + sink = require('./mongo'); +} else if (config.getDatabase().type === 'fs') { + sink = require('./file'); +} + +export const createUser = async ( + username: string, + password: string, + email: string, + gitAccount: string, + admin: boolean = false, + oidcId: string = '', +) => { + console.log( + `creating user + user=${username}, + gitAccount=${gitAccount} + email=${email}, + admin=${admin} + oidcId=${oidcId}`, + ); + + const data = { + username: username, + password: oidcId ? null : await bcrypt.hash(password, 10), + gitAccount: gitAccount, + email: email, + admin: admin, + }; + + if (username === undefined || username === null || username === '') { + const errorMessage = `username ${username} cannot be empty`; + throw new Error(errorMessage); + } + + if (gitAccount === undefined || gitAccount === null || gitAccount === '') { + const errorMessage = `GitAccount ${gitAccount} cannot be empty`; + throw new Error(errorMessage); + } + + if (email === undefined || email === null || email === '') { + const errorMessage = `Email ${email} cannot be empty`; + throw new Error(errorMessage); + } + const existingUser = await sink.findUser(username); + + if (existingUser) { + const errorMessage = `user ${username} already exists`; + throw new Error(errorMessage); + } + + await sink.createUser(data); +}; + +export const { + authorise, + reject, + cancel, + getPushes, + writeAudit, + getPush, + findUser, + findUserByOIDC, + getUsers, + deleteUser, + updateUser, + getRepos, + getRepo, + createRepo, + addUserCanPush, + addUserCanAuthorise, + removeUserCanAuthorise, + removeUserCanPush, + deleteRepo, + isUserPushAllowed, + canUserApproveRejectPushRepo, + canUserApproveRejectPush, + canUserCancelPush, + getSessionStore, +} = sink; diff --git a/src/db/mongo/helper.js b/src/db/mongo/helper.js deleted file mode 100644 index 116b35c08..000000000 --- a/src/db/mongo/helper.js +++ /dev/null @@ -1,26 +0,0 @@ -const mongo = require('mongodb'); -const config = require('../../config'); -const dbConfig = config.getDatabase(); -const options = dbConfig.options; -const connectionString = dbConfig.connectionString; -const MongoDBStore = require('connect-mongo'); - -let _db; - -exports.connect = async (collectionName) => { - if (!_db) { - const client = new mongo.MongoClient(connectionString, options); - await client.connect(); - _db = await client.db(); - } - - return _db.collection(collectionName); -}; - -exports.getSessionStore = (session) => { - return new MongoDBStore({ - mongoUrl: connectionString, - collectionName: 'user_session', - mongoOptions: options, - }); -}; diff --git a/src/db/mongo/helper.ts b/src/db/mongo/helper.ts new file mode 100644 index 000000000..335434ebb --- /dev/null +++ b/src/db/mongo/helper.ts @@ -0,0 +1,49 @@ +import { MongoClient, Db, Collection, Filter, Document, FindOptions } from 'mongodb'; +import { getDatabase } from '../../config'; +import MongoDBStore from 'connect-mongo'; + +const dbConfig = getDatabase(); +const connectionString = dbConfig.connectionString; +const options = dbConfig.options; + +let _db: Db | null = null; + +export const connect = async (collectionName: string): Promise => { + if (!_db) { + if (!connectionString) { + throw new Error('MongoDB connection string is not provided'); + } + + const client = new MongoClient(connectionString, options); + await client.connect(); + _db = client.db(); + } + + return _db.collection(collectionName); +}; + +export const findDocuments = async ( + collectionName: string, + filter: Filter = {}, + options: FindOptions = {} +): Promise => { + const collection = await connect(collectionName); + return collection.find(filter, options).toArray() as Promise; +}; + +export const findOneDocument = async ( + collectionName: string, + filter: Filter = {}, + options: FindOptions = {} +): Promise => { + const collection = await connect(collectionName); + return (await collection.findOne(filter, options)) as T | null; +}; + +export const getSessionStore = () => { + return new MongoDBStore({ + mongoUrl: connectionString, + collectionName: 'user_session', + mongoOptions: options, + }); +}; diff --git a/src/db/mongo/index.js b/src/db/mongo/index.js deleted file mode 100644 index d8687f30c..000000000 --- a/src/db/mongo/index.js +++ /dev/null @@ -1,32 +0,0 @@ -const pushes = require('./pushes'); -const users = require('./users'); -const repo = require('./repo'); -const helper = require('./helper'); - -module.exports.getPushes = pushes.getPushes; -module.exports.writeAudit = pushes.writeAudit; -module.exports.getPush = pushes.getPush; -module.exports.authorise = pushes.authorise; -module.exports.cancel = pushes.cancel; -module.exports.reject = pushes.reject; -module.exports.canUserApproveRejectPush = pushes.canUserApproveRejectPush; -module.exports.canUserCancelPush = pushes.canUserCancelPush; - -module.exports.findUser = users.findUser; -module.exports.getUsers = users.getUsers; -module.exports.createUser = users.createUser; -module.exports.deleteUser = users.deleteUser; -module.exports.updateUser = users.updateUser; - -module.exports.getRepos = repo.getRepos; -module.exports.getRepo = repo.getRepo; -module.exports.createRepo = repo.createRepo; -module.exports.addUserCanPush = repo.addUserCanPush; -module.exports.addUserCanAuthorise = repo.addUserCanAuthorise; -module.exports.removeUserCanPush = repo.removeUserCanPush; -module.exports.removeUserCanAuthorise = repo.removeUserCanAuthorise; -module.exports.deleteRepo = repo.deleteRepo; -module.exports.isUserPushAllowed = repo.isUserPushAllowed; -module.exports.canUserApproveRejectPushRepo = repo.canUserApproveRejectPushRepo; - -module.exports.getSessionStore = helper.getSessionStore; diff --git a/src/db/mongo/index.ts b/src/db/mongo/index.ts new file mode 100644 index 000000000..11f526c2a --- /dev/null +++ b/src/db/mongo/index.ts @@ -0,0 +1,40 @@ +import * as helper from './helper'; +import * as pushes from './pushes'; +import * as repo from './repo'; +import * as users from './users'; + +export const { + getSessionStore, +} = helper; + +export const { + getPushes, + writeAudit, + getPush, + authorise, + cancel, + reject, + canUserCancelPush, + canUserApproveRejectPush, +} = pushes; + +export const { + getRepos, + getRepo, + createRepo, + addUserCanPush, + addUserCanAuthorise, + removeUserCanPush, + removeUserCanAuthorise, + deleteRepo, + isUserPushAllowed, + canUserApproveRejectPushRepo, +} = repo; + +export const { + findUser, + getUsers, + createUser, + deleteUser, + updateUser, +} = users; diff --git a/src/db/mongo/pushes.js b/src/db/mongo/pushes.js deleted file mode 100644 index f92d7ba52..000000000 --- a/src/db/mongo/pushes.js +++ /dev/null @@ -1,126 +0,0 @@ -const connect = require('./helper').connect; -const Action = require('../../proxy/actions').Action; -const toClass = require('../helper').toClass; -const repo = require('./repo'); -const cnName = 'pushes'; - -const defaultPushQuery = { - error: false, - blocked: true, - allowPush: false, - authorised: false, -}; - -const getPushes = async (query = defaultPushQuery) => { - const collection = await connect(cnName); - return collection - .find(query, { - projection: { - _id: 0, - id: 1, - allowPush: 1, - authorised: 1, - blocked: 1, - blockedMessage: 1, - branch: 1, - canceled: 1, - commitData: 1, - commitFrom: 1, - commitTo: 1, - error: 1, - method: 1, - project: 1, - rejected: 1, - repo: 1, - repoName: 1, - timepstamp: 1, - type: 1, - url: 1, - }, - }) - .toArray(); -}; - -const getPush = async (id) => { - const collection = await connect(cnName); - const doc = await collection.findOne({ id: id }); - - if (doc) { - return toClass(doc, Action.prototype); - } - - return null; -}; - -const writeAudit = async (action) => { - const data = JSON.parse(JSON.stringify(action)); - const options = { upsert: true }; - const collection = await connect(cnName); - delete data._id; - if (typeof data.id !== 'string') { - throw new Error('Invalid id'); - } - await collection.updateOne({ id: { $eq: data.id } }, { $set: data }, options); - return action; -}; - -const authorise = async (id, attestation) => { - const action = await getPush(id); - action.authorised = true; - action.canceled = false; - action.rejected = false; - action.attestation = attestation; - await writeAudit(action); - return { message: `authorised ${id}` }; -}; - -const reject = async (id) => { - const action = await getPush(id); - action.authorised = false; - action.canceled = false; - action.rejected = true; - await writeAudit(action); - return { message: `reject ${id}` }; -}; - -const cancel = async (id) => { - const action = await getPush(id); - action.authorised = false; - action.canceled = true; - action.rejected = false; - await writeAudit(action); - return { message: `canceled ${id}` }; -}; - -const canUserApproveRejectPush = async (id, user) => { - return new Promise(async (resolve) => { - const action = await getPush(id); - const repoName = action.repoName.replace('.git', ''); - const isAllowed = await repo.canUserApproveRejectPushRepo(repoName, user); - - resolve(isAllowed); - }); -}; - -const canUserCancelPush = async (id, user) => { - return new Promise(async (resolve) => { - const pushDetail = await getPush(id); - const repoName = pushDetail.repoName.replace('.git', ''); - const isAllowed = await repo.isUserPushAllowed(repoName, user); - - if (isAllowed) { - resolve(true); - } else { - resolve(false); - } - }); -}; - -module.exports.getPushes = getPushes; -module.exports.writeAudit = writeAudit; -module.exports.getPush = getPush; -module.exports.authorise = authorise; -module.exports.reject = reject; -module.exports.cancel = cancel; -module.exports.canUserApproveRejectPush = canUserApproveRejectPush; -module.exports.canUserCancelPush = canUserCancelPush; diff --git a/src/db/mongo/pushes.ts b/src/db/mongo/pushes.ts new file mode 100644 index 000000000..609db1252 --- /dev/null +++ b/src/db/mongo/pushes.ts @@ -0,0 +1,132 @@ +import { connect, findDocuments, findOneDocument } from './helper'; +import { Action } from '../../proxy/actions'; +import { toClass } from '../helper'; +import * as repo from './repo'; +import { Push, PushQuery } from '../types'; + +const collectionName = 'pushes'; + +const defaultPushQuery: PushQuery = { + error: false, + blocked: true, + allowPush: false, + authorised: false, +}; + +export const getPushes = async ( + query: PushQuery = defaultPushQuery +): Promise => { + return findDocuments(collectionName, query, { + projection: { + _id: 0, + id: 1, + allowPush: 1, + authorised: 1, + blocked: 1, + blockedMessage: 1, + branch: 1, + canceled: 1, + commitData: 1, + commitFrom: 1, + commitTo: 1, + error: 1, + method: 1, + project: 1, + rejected: 1, + repo: 1, + repoName: 1, + timepstamp: 1, + type: 1, + url: 1, + }, + }); +}; + +export const getPush = async (id: string): Promise => { + const doc = await findOneDocument(collectionName, { id }); + return doc ? toClass(doc, Action.prototype) as Action : null; +}; + +export const writeAudit = async (action: Action): Promise => { + const data = JSON.parse(JSON.stringify(action)); + const options = { upsert: true }; + const collection = await connect(collectionName); + delete data._id; + if (typeof data.id !== 'string') { + throw new Error('Invalid id'); + } + await collection.updateOne({ id: data.id }, { $set: data }, options); + return action; +}; + +export const authorise = async (id: string, attestation: any) => { + const action = await getPush(id); + if (!action) { + throw new Error(`push ${id} not found`); + } + + action.authorised = true; + action.canceled = false; + action.rejected = false; + action.attestation = attestation; + await writeAudit(action); + return { message: `authorised ${id}` }; +}; + +export const reject = async (id: string) => { + const action = await getPush(id); + if (!action) { + throw new Error(`push ${id} not found`); + } + action.authorised = false; + action.canceled = false; + action.rejected = true; + await writeAudit(action); + return { message: `reject ${id}` }; +}; + +export const cancel = async (id: string) => { + const action = await getPush(id); + if (!action) { + throw new Error(`push ${id} not found`); + } + action.authorised = false; + action.canceled = true; + action.rejected = false; + await writeAudit(action); + return { message: `canceled ${id}` }; +}; + +export const canUserApproveRejectPush = async (id: string, user: string) => { + return new Promise(async (resolve) => { + const action = await getPush(id); + if (!action) { + resolve(false); + return; + } + + const repoName = action.repoName.replace('.git', ''); + const isAllowed = await repo.canUserApproveRejectPushRepo(repoName, user); + + resolve(isAllowed); + }); +}; + +export const canUserCancelPush = async (id: string, user: string) => { + return new Promise(async (resolve) => { + const pushDetail = await getPush(id); + if (!pushDetail) { + resolve(false); + return; + } + + const repoName = pushDetail.repoName.replace('.git', ''); + const isAllowed = await repo.isUserPushAllowed(repoName, user); + + if (isAllowed) { + resolve(true); + } else { + resolve(false); + } + }); +}; diff --git a/src/db/mongo/repo.js b/src/db/mongo/repo.ts similarity index 59% rename from src/db/mongo/repo.js rename to src/db/mongo/repo.ts index 1c604069e..5e55d3d71 100644 --- a/src/db/mongo/repo.js +++ b/src/db/mongo/repo.ts @@ -1,21 +1,23 @@ +import { Repo } from "../types"; + const connect = require('./helper').connect; -const cnName = 'repos'; +const collectionName = 'repos'; -const isBlank = (str) => { +const isBlank = (str: string) => { return !str || /^\s*$/.test(str); }; -exports.getRepos = async (query = {}) => { - const collection = await connect(cnName); - return collection.find().toArray(); +export const getRepos = async (query: any = {}) => { + const collection = await connect(collectionName); + return collection.find(query).toArray(); }; -exports.getRepo = async (name) => { - const collection = await connect(cnName); +export const getRepo = async (name: string) => { + const collection = await connect(collectionName); return collection.findOne({ name: { $eq: name } }); }; -exports.createRepo = async (repo) => { +export const createRepo = async (repo: Repo) => { console.log(`creating new repo ${JSON.stringify(repo)}`); if (isBlank(repo.project)) { @@ -33,43 +35,43 @@ exports.createRepo = async (repo) => { canAuthorise: [], }; - const collection = await connect(cnName); + const collection = await connect(collectionName); await collection.insertOne(repo); console.log(`created new repo ${JSON.stringify(repo)}`); }; -exports.addUserCanPush = async (name, user) => { +export const addUserCanPush = async (name: string, user: string) => { name = name.toLowerCase(); - const collection = await connect(cnName); + const collection = await connect(collectionName); await collection.updateOne({ name: name }, { $push: { 'users.canPush': user } }); }; -exports.addUserCanAuthorise = async (name, user) => { +export const addUserCanAuthorise = async (name: string, user: string) => { name = name.toLowerCase(); - const collection = await connect(cnName); + const collection = await connect(collectionName); await collection.updateOne({ name: name }, { $push: { 'users.canAuthorise': user } }); }; -exports.removeUserCanPush = async (name, user) => { +export const removeUserCanPush = async (name: string, user: string) => { name = name.toLowerCase(); - const collection = await connect(cnName); + const collection = await connect(collectionName); await collection.updateOne({ name: name }, { $pull: { 'users.canPush': user } }); }; -exports.removeUserCanAuthorise = async (name, user) => { +export const removeUserCanAuthorise = async (name: string, user: string) => { name = name.toLowerCase(); - const collection = await connect(cnName); + const collection = await connect(collectionName); await collection.updateOne({ name: name }, { $pull: { 'users.canAuthorise': user } }); }; -exports.deleteRepo = async (name) => { - const collection = await connect(cnName); +export const deleteRepo = async (name: string) => { + const collection = await connect(collectionName); await collection.deleteMany({ name: name }); }; -exports.isUserPushAllowed = async (name, user) => { +export const isUserPushAllowed = async (name: string, user: string) => { name = name.toLowerCase(); - return new Promise(async (resolve, reject) => { + return new Promise(async (resolve) => { const repo = await exports.getRepo(name); console.log(repo.users.canPush); console.log(repo.users.canAuthorise); @@ -82,10 +84,10 @@ exports.isUserPushAllowed = async (name, user) => { }); }; -exports.canUserApproveRejectPushRepo = async (name, user) => { +export const canUserApproveRejectPushRepo = async (name: string, user: string) => { name = name.toLowerCase(); console.log(`checking if user ${user} can approve/reject for ${name}`); - return new Promise(async (resolve, reject) => { + return new Promise(async (resolve) => { const repo = await exports.getRepo(name); if (repo.users.canAuthorise.includes(user)) { console.log(`user ${user} can approve/reject to repo ${name}`); diff --git a/src/db/mongo/users.js b/src/db/mongo/users.js deleted file mode 100644 index c93977694..000000000 --- a/src/db/mongo/users.js +++ /dev/null @@ -1,31 +0,0 @@ -const connect = require('./helper').connect; -const usersCollection = 'users'; - -exports.findUser = async function (username) { - const collection = await connect(usersCollection); - return collection.findOne({ username: { $eq: username } }); -}; - -exports.getUsers = async function (query) { - console.log(`Getting users for query= ${JSON.stringify(query)}`); - const collection = await connect(usersCollection); - return collection.find(query, { password: 0 }).toArray(); -}; - -exports.deleteUser = async function (username) { - const collection = await connect(usersCollection); - return collection.deleteOne({ username: username }); -}; - -exports.createUser = async function (data) { - data.username = data.username.toLowerCase(); - const collection = await connect(usersCollection); - return collection.insertOne(data); -}; - -exports.updateUser = async (user) => { - user.username = user.username.toLowerCase(); - const options = { upsert: true }; - const collection = await connect(usersCollection); - await collection.updateOne({ username: user.username }, { $set: user }, options); -}; diff --git a/src/db/mongo/users.ts b/src/db/mongo/users.ts new file mode 100644 index 000000000..0bfa1a941 --- /dev/null +++ b/src/db/mongo/users.ts @@ -0,0 +1,33 @@ +import { User } from "../types"; + +const connect = require('./helper').connect; +const collectionName = 'users'; + +export const findUser = async function (username: string) { + const collection = await connect(collectionName); + return collection.findOne({ username: { $eq: username } }); +}; + +export const getUsers = async function (query: any = {}) { + console.log(`Getting users for query= ${JSON.stringify(query)}`); + const collection = await connect(collectionName); + return collection.find(query, { password: 0 }).toArray(); +}; + +export const deleteUser = async function (username: string) { + const collection = await connect(collectionName); + return collection.deleteOne({ username: username }); +}; + +export const createUser = async function (user: User) { + user.username = user.username.toLowerCase(); + const collection = await connect(collectionName); + return collection.insertOne(user); +}; + +export const updateUser = async (user: User) => { + user.username = user.username.toLowerCase(); + const options = { upsert: true }; + const collection = await connect(collectionName); + await collection.updateOne({ username: user.username }, { $set: user }, options); +}; diff --git a/src/db/types.ts b/src/db/types.ts new file mode 100644 index 000000000..dba9bdf3a --- /dev/null +++ b/src/db/types.ts @@ -0,0 +1,48 @@ +export type PushQuery = { + error: boolean; + blocked: boolean, + allowPush: boolean, + authorised: boolean +}; + +export type UserRole = 'canPush' | 'canAuthorise'; + +export type Repo = { + project: string; + name: string; + url: string; + users: Record; + _id: string; +}; + +export type User = { + _id: string; + username: string; + password: string | null; // null if oidcId is set + gitAccount: string; + email: string; + admin: boolean; + oidcId: string | null; +}; + +export type Push = { + id: string; + allowPush: boolean; + authorised: boolean; + blocked: boolean; + blockedMessage: string; + branch: string; + canceled: boolean; + commitData: object; + commitFrom: string; + commitTo: string; + error: boolean; + method: string; + project: string; + rejected: boolean; + repo: string; + repoName: string; + timepstamp: string; + type: string; + url: string; +}; diff --git a/src/plugin.js b/src/plugin.ts similarity index 75% rename from src/plugin.js rename to src/plugin.ts index b02e3bb8e..f2bd8f26a 100644 --- a/src/plugin.js +++ b/src/plugin.ts @@ -1,4 +1,7 @@ +import { Action } from './proxy/actions'; + const lpModule = import('load-plugin'); +/* eslint-disable @typescript-eslint/no-unused-expressions */ ('use strict'); /** @@ -7,7 +10,7 @@ const lpModule = import('load-plugin'); * @param {string} propertyName - The property name to check for. Default is 'isGitProxyPlugin'. * @return {boolean} - True if the object or any of its prototypes has the 'isGitProxyPlugin' property set to true, false otherwise. */ -function isCompatiblePlugin(obj, propertyName = 'isGitProxyPlugin') { +function isCompatiblePlugin(obj: any, propertyName: string = 'isGitProxyPlugin'): boolean { // loop through the prototype chain to check if the object is a ProxyPlugin // valid plugin objects will have the appropriate property set to true // if the prototype chain is exhausted, return false @@ -22,36 +25,24 @@ function isCompatiblePlugin(obj, propertyName = 'isGitProxyPlugin') { return false; } -/** - * @typedef PluginTypeResult - * @property {PushActionPlugin[]} pushAction - List of push action plugins - * @property {PullActionPlugin[]} pullAction - List of pull action plugins - */ +interface PluginTypeResult { + pushAction: PushActionPlugin[]; + pullAction: PullActionPlugin[]; +} /** * Registers and loads plugins used by git-proxy */ class PluginLoader { - constructor(targets) { - /** - * List of Node module specifiers to load as plugins. It can be a relative path, an - * absolute path, or a module name (which can include scoped packages like '@bar/baz'). - * @type {string[]} - * @public - */ + targets: string[]; + pushPlugins: PushActionPlugin[]; + pullPlugins: PullActionPlugin[]; + + constructor(targets: string[]) { this.targets = targets; - /** - * List of loaded PushActionPlugin objects. - * @type {PushActionPlugin[]} - * @public - */ this.pushPlugins = []; - /** - * List of loaded PullActionPlugin objects. - * @type {PullActionPlugin[]} - * @public - */ this.pullPlugins = []; + if (this.targets.length === 0) { console.log('No plugins configured'); // TODO: log.debug() } @@ -62,7 +53,7 @@ class PluginLoader { * can be used to retrieve plugins. * @return {Promise} A Promise that resolves when all plugins have been loaded. */ - async load() { + async load(): Promise { try { const modulePromises = this.targets.map(target => this._loadPluginModule(target).catch(error => { @@ -73,7 +64,7 @@ class PluginLoader { const moduleResults = await Promise.allSettled(modulePromises); const loadedModules = moduleResults - .filter(result => result.status === 'fulfilled' && result.value !== null) + .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled' && result.value !== null) .map(result => result.value); console.log(`Found ${loadedModules.length} plugin modules`); // TODO: log.debug() @@ -90,7 +81,7 @@ class PluginLoader { * @type {PluginTypeResult[]} List of resolved PluginTypeResult objects */ const pluginTypeResults = settledPluginTypeResults - .filter(result => result.status === 'fulfilled' && result.value !== null) + .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled' && result.value !== null) .map(result => result.value); for (const result of pluginTypeResults) { @@ -112,7 +103,7 @@ class PluginLoader { * @param {string} target The module specifier to load * @return {Promise} A resolved & loaded Module */ - async _loadPluginModule(target) { + private async _loadPluginModule(target: string): Promise { const lp = await lpModule; const resolvedModuleFile = await lp.resolvePlugin(target); return await lp.loadPlugin(resolvedModuleFile); @@ -123,13 +114,13 @@ class PluginLoader { * @param {Module} pluginModule The module to extract plugins from * @return {Promise} An object containing the loaded plugins classified by their type. */ - async _getPluginObjects(pluginModule) { - const plugins = { + private async _getPluginObjects(pluginModule: any): Promise { + const plugins: PluginTypeResult = { pushAction: [], pullAction: [], }; - function handlePlugin(potentialModule) { + function handlePlugin(potentialModule: any) { if (isCompatiblePlugin(potentialModule, 'isGitProxyPushActionPlugin')) { console.log('found push plugin', potentialModule.constructor.name); plugins.pushAction.push(potentialModule); @@ -164,6 +155,8 @@ class PluginLoader { * ProxyPlugin to be loaded by PluginLoader. */ class ProxyPlugin { + isGitProxyPlugin: boolean; + constructor() { this.isGitProxyPlugin = true; } @@ -173,21 +166,24 @@ class ProxyPlugin { * A plugin which executes a function when receiving a git push request. */ class PushActionPlugin extends ProxyPlugin { -/** - * Wrapper class which contains at least one function executed as part of the action chain for git push operations. - * The function must be called `exec` and take in two parameters: an Express Request (req) and the current Action - * executed in the chain (action). This function should return a Promise that resolves to an Action. - * - * Optionally, child classes which extend this can simply define the `exec` function as their own property. - * This is the preferred implementation when a custom plugin (subclass) has its own state or additional methods - * that are required. - * - * @param {function} exec - A function that: - * - Takes in an Express Request object as the first parameter (`req`). - * - Takes in an Action object as the second parameter (`action`). - * - Returns a Promise that resolves to an Action. - */ - constructor(exec) { + isGitProxyPushActionPlugin: boolean; + exec: (req: any, action: Action) => Promise; + + /** + * Wrapper class which contains at least one function executed as part of the action chain for git push operations. + * The function must be called `exec` and take in two parameters: an Express Request (req) and the current Action + * executed in the chain (action). This function should return a Promise that resolves to an Action. + * + * Optionally, child classes which extend this can simply define the `exec` function as their own property. + * This is the preferred implementation when a custom plugin (subclass) has its own state or additional methods + * that are required. + * + * @param {function} exec - A function that: + * - Takes in an Express Request object as the first parameter (`req`). + * - Takes in an Action object as the second parameter (`action`). + * - Returns a Promise that resolves to an Action. + */ + constructor(exec: (req: any, action: Action) => Promise) { super(); this.isGitProxyPushActionPlugin = true; this.exec = exec; @@ -198,6 +194,9 @@ class PushActionPlugin extends ProxyPlugin { * A plugin which executes a function when receiving a git fetch request. */ class PullActionPlugin extends ProxyPlugin { + isGitProxyPullActionPlugin: boolean; + exec: (req: any, action: Action) => Promise; + /** * Wrapper class which contains at least one function executed as part of the action chain for git pull operations. * The function must be called `exec` and take in two parameters: an Express Request (req) and the current Action @@ -212,14 +211,14 @@ class PullActionPlugin extends ProxyPlugin { * - Takes in an Action object as the second parameter (`action`). * - Returns a Promise that resolves to an Action. */ - constructor(exec) { + constructor(exec: (req: any, action: Action) => Promise) { super(); this.isGitProxyPullActionPlugin = true; this.exec = exec; } } -module.exports = { +export { PluginLoader, PushActionPlugin, PullActionPlugin, diff --git a/src/proxy/actions/Action.js b/src/proxy/actions/Action.js deleted file mode 100644 index ba34f2938..000000000 --- a/src/proxy/actions/Action.js +++ /dev/null @@ -1,130 +0,0 @@ -/** Class representing a Push. */ -const config = require('../../config'); - -/** - * Create a new action - */ -class Action { - steps = []; - error = false; - errorMessage; - blocked = false; - blockedMessage; - allowPush = false; - authorised = false; - canceled = false; - rejected = false; - autoApproved = false; - autoRejected = false; - commitFrom; - commitTo; - branch; - message; - author; - user; - attestation; - - /** - * - * @param {*} id - * @param {*} type - * @param {*} method - * @param {*} timestamp - * @param {*} repo - */ - constructor(id, type, method, timestamp, repo) { - this.id = id; - this.type = type; - this.method = method; - this.timestamp = timestamp; - this.project = repo.split('/')[0]; - this.repoName = repo.split('/')[1]; - this.url = `${config.getProxyUrl()}/${repo}`; - this.repo = repo; - } - - /** - * - * @param {*} step - */ - addStep(step) { - this.steps.push(step); - this.lastStep = step; - - if (step.blocked) { - this.blocked = true; - this.blockedMessage = step.blockedMessage; - } - - if (step.error) { - this.error = true; - this.errorMessage = step.errorMessage; - } - } - - /** - * - * @param {*} step - * @return {Step} - */ - getLastStep(step) { - return this.lastStep; - } - - /** - * - * @param {string} commitFrom - * @param {string} commitTo - */ - setCommit(commitFrom, commitTo) { - this.commitFrom = commitFrom; - this.commitTo = commitTo; - this.id = `${commitFrom}__${commitTo}`; - } - - /** - * - * @param {string} branch - */ - setBranch(branch) { - this.branch = branch; - } - - /** - * - * @param {*} message - */ - setMessage(message) { - this.message = message; - } - - /** - *` - */ - setAllowPush() { - this.allowPush = true; - this.blocked = false; - } - - /** - *` - */ - setAutoApproval() { - this.autoApproved = true; - } - - /** - *` - */ - setAutoRejection() { - this.autoRejected = true; - } - /** - * @return {bool} - */ - continue() { - return !(this.error || this.blocked); - } -} - -exports.Action = Action; diff --git a/src/proxy/actions/Action.ts b/src/proxy/actions/Action.ts new file mode 100644 index 000000000..78dbc2ef0 --- /dev/null +++ b/src/proxy/actions/Action.ts @@ -0,0 +1,156 @@ +import { getProxyUrl } from "../../config"; +import { Step } from "./Step"; + +/** + * Represents a commit. + */ +export interface Commit { + message: string; + committer: string; + tree: string; + parent: string; + author: string; + authorEmail: string; + commitTS?: string; // TODO: Normalize this to commitTimestamp + commitTimestamp?: string; +} + +/** + * Class representing a Push. + */ +class Action { + id: string; + type: string; + method: string; + timestamp: number; + project: string; + repoName: string; + url: string; + repo: string; + steps: Step[] = []; + error: boolean = false; + errorMessage?: string | null; + blocked: boolean = false; + blockedMessage?: string | null; + allowPush: boolean = false; + authorised: boolean = false; + canceled: boolean = false; + rejected: boolean = false; + autoApproved: boolean = false; + autoRejected: boolean = false; + commitData?: Commit[] = []; + commitFrom?: string; + commitTo?: string; + branch?: string; + message?: string; + author?: string; + user?: string; + attestation?: string; + lastStep?: Step; + proxyGitPath?: string; + + /** + * Create an action. + * @param {string} id The id of the action + * @param {string} type The type of the action + * @param {string} method The method of the action + * @param {number} timestamp The timestamp of the action + * @param {string} repo The repo of the action + */ + constructor(id: string, type: string, method: string, timestamp: number, repo: string) { + this.id = id; + this.type = type; + this.method = method; + this.timestamp = timestamp; + this.project = repo.split("/")[0]; + this.repoName = repo.split("/")[1]; + this.url = `${getProxyUrl()}/${repo}`; + this.repo = repo; + } + + /** + * Add a step to the action. + * @param {Step} step + */ + addStep(step: Step): void { + this.steps.push(step); + this.lastStep = step; + + if (step.blocked) { + this.blocked = true; + this.blockedMessage = step.blockedMessage; + } + + if (step.error) { + this.error = true; + this.errorMessage = step.errorMessage; + } + } + + /** + * Get the last step of the action. + * @return {Step} The last step of the action + */ + getLastStep(): Step | undefined { + return this.lastStep; + } + + /** + * Set the commit range for the action. + * @param {string} commitFrom the starting commit + * @param {string} commitTo the ending commit + */ + setCommit(commitFrom: string, commitTo: string): void { + this.commitFrom = commitFrom; + this.commitTo = commitTo; + this.id = `${commitFrom}__${commitTo}`; + } + + /** + * Set the branch for the action. + * @param {string} branch the branch + */ + setBranch(branch: string): void { + this.branch = branch; + } + + /** + * Set the message for the action. + * @param {string} message the message + */ + setMessage(message: string): void { + this.message = message; + } + + /** + * Allow the action to continue. + */ + setAllowPush(): void { + this.allowPush = true; + this.blocked = false; + } + + /** + * Set auto approval for the action. + */ + setAutoApproval(): void { + this.autoApproved = true; + } + + /** + * Set auto rejection for the action. + */ + setAutoRejection(): void { + this.autoRejected = true; + } + + /** + * Check if the action can continue. + * @return {boolean} true if the action can continue, false otherwise + */ + continue(): boolean { + return !(this.error || this.blocked); + } +} + +export { Action }; diff --git a/src/proxy/actions/Step.js b/src/proxy/actions/Step.js deleted file mode 100644 index 96a17a2ff..000000000 --- a/src/proxy/actions/Step.js +++ /dev/null @@ -1,75 +0,0 @@ -const { v4: uuidv4 } = require('uuid'); - -/** Class representing a Push. */ -class Step { - logs = []; - - /** - * - * @param {*} stepName - * @param {*} error - * @param {*} errorMessage - * @param {*} blocked - * @param {*} blockedMessage - * @param {*} content - */ - constructor( - stepName, - error = false, - errorMessage = null, - blocked = false, - blockedMessage = null, - content = null, - ) { - this.id = uuidv4(); - this.stepName = stepName; - this.content = content; - - this.error = error; - this.errorMessage = errorMessage; - - this.blocked = blocked; - this.blockedMessage = blockedMessage; - } - - /** - * - * @param {*} message - */ - setError(message) { - this.error = true; - this.errorMessage = message; - this.log(message); - } - - /** - * - * @param {*} content - */ - setContent(content) { - this.log('setting content'); - this.content = content; - } - - /** - * - * @param {*} message - */ - setAsyncBlock(message) { - this.log('setting blocked'); - this.blocked = true; - this.blockedMessage = message; - } - - /** - * - * @param {*} message - */ - log(message) { - const m = `${this.stepName} - ${message}`; - this.logs.push(m); - console.info(m); - } -} - -exports.Step = Step; diff --git a/src/proxy/actions/Step.ts b/src/proxy/actions/Step.ts new file mode 100644 index 000000000..cdb07bf95 --- /dev/null +++ b/src/proxy/actions/Step.ts @@ -0,0 +1,55 @@ +import { v4 as uuidv4 } from "uuid"; + +/** Class representing a Push Step. */ +class Step { + id: string; + stepName: string; + content: any; + error: boolean; + errorMessage: string | null; + blocked: boolean; + blockedMessage: string | null; + logs: string[] = []; + + constructor( + stepName: string, + error: boolean = false, + errorMessage: string | null = null, + blocked: boolean = false, + blockedMessage: string | null = null, + content: any = null + ) { + this.id = uuidv4(); + this.stepName = stepName; + this.content = content; + this.error = error; + this.errorMessage = errorMessage; + this.blocked = blocked; + this.blockedMessage = blockedMessage; + } + + setError(message: string): void { + this.error = true; + this.errorMessage = message; + this.log(message); + } + + setContent(content: any): void { + this.log("setting content"); + this.content = content; + } + + setAsyncBlock(message: string): void { + this.log("setting blocked"); + this.blocked = true; + this.blockedMessage = message; + } + + log(message: string): void { + const m = `${this.stepName} - ${message}`; + this.logs.push(m); + console.info(m); + } +} + +export { Step }; diff --git a/src/proxy/actions/autoActions.js b/src/proxy/actions/autoActions.ts similarity index 63% rename from src/proxy/actions/autoActions.js rename to src/proxy/actions/autoActions.ts index 0591ebe82..03ed0529a 100644 --- a/src/proxy/actions/autoActions.js +++ b/src/proxy/actions/autoActions.ts @@ -1,38 +1,39 @@ -const db = require('../../db'); +import { authorise, reject } from '../../db'; +import { Action } from './Action'; -const attemptAutoApproval = async (action) => { +const attemptAutoApproval = async (action: Action) => { try { const attestation = { timestamp: new Date(), autoApproved: true, }; - await db.authorise(action.id, attestation); + await authorise(action.id, attestation); console.log('Push automatically approved by system.'); return true; - } catch (error) { + } catch (error: any) { console.error('Error during auto-approval:', error.message); return false; } }; -const attemptAutoRejection = async (action) => { +const attemptAutoRejection = async (action: Action) => { try { const attestation = { timestamp: new Date(), autoApproved: true, }; - await db.reject(action.id, attestation); + await reject(action.id, attestation); console.log('Push automatically rejected by system.'); return true; - } catch (error) { + } catch (error: any) { console.error('Error during auto-rejection:', error.message); return false; } }; -module.exports = { +export { attemptAutoApproval, attemptAutoRejection, }; diff --git a/src/proxy/actions/index.js b/src/proxy/actions/index.js deleted file mode 100644 index 173bb158b..000000000 --- a/src/proxy/actions/index.js +++ /dev/null @@ -1,7 +0,0 @@ -const { Action } = require('./Action'); -const { Step } = require('./Step'); - -module.exports = { - Action, - Step, -} diff --git a/src/proxy/actions/index.ts b/src/proxy/actions/index.ts new file mode 100644 index 000000000..72aa2918a --- /dev/null +++ b/src/proxy/actions/index.ts @@ -0,0 +1,7 @@ +import { Action } from './Action'; +import { Step } from './Step'; + +export { + Action, + Step +} diff --git a/src/proxy/chain.js b/src/proxy/chain.ts similarity index 73% rename from src/proxy/chain.js rename to src/proxy/chain.ts index c9304d330..41a7cc495 100644 --- a/src/proxy/chain.js +++ b/src/proxy/chain.ts @@ -1,7 +1,9 @@ -const proc = require('./processors'); -const { attemptAutoApproval, attemptAutoRejection } = require('./actions/autoActions'); +import { PluginLoader } from '../plugin'; +import { Action } from './actions'; +import * as proc from './processors'; +import { attemptAutoApproval, attemptAutoRejection } from './actions/autoActions'; -const pushActionChain = [ +const pushActionChain: ((req: any, action: Action) => Promise)[] = [ proc.push.parsePush, proc.push.checkRepoInAuthorisedList, proc.push.checkCommitMessages, @@ -17,19 +19,17 @@ const pushActionChain = [ proc.push.blockForAuth, ]; -const pullActionChain = [proc.push.checkRepoInAuthorisedList]; +const pullActionChain: ((req: any, action: Action) => Promise)[] = [proc.push.checkRepoInAuthorisedList]; let pluginsInserted = false; -const executeChain = async (req) => { - let action; +export const executeChain = async (req: any, res: any): Promise => { + let action: Action = {} as Action; try { action = await proc.pre.parseAction(req); - const actions = await getChain(action); - for (const i in actions) { - if (!i) continue; - const fn = actions[i]; + const actionFns = await getChain(action); + for (const fn of actionFns) { action = await fn(req, action); if (!action.continue()) { return action; @@ -55,9 +55,9 @@ const executeChain = async (req) => { * The plugin loader used for the GitProxy chain. * @type {import('../plugin').PluginLoader} */ -let chainPluginLoader; +let chainPluginLoader: PluginLoader; -const getChain = async (action) => { +const getChain = async (action: Action): Promise<((req: any, action: Action) => Promise)[]> => { if (chainPluginLoader === undefined) { console.error( 'Plugin loader was not initialized! This is an application error. Please report it to the GitProxy maintainers. Skipping plugins...', @@ -81,16 +81,12 @@ const getChain = async (action) => { // This is set to true so that we don't re-insert the plugins into the chain pluginsInserted = true; } - if (action.type === 'pull') { - return pullActionChain; - } - if (action.type === 'push') { - return pushActionChain; - } - if (action.type === 'default') return []; + if (action.type === 'pull') return pullActionChain; + if (action.type === 'push') return pushActionChain; + return []; }; -module.exports = { +export default { set chainPluginLoader(loader) { chainPluginLoader = loader; }, diff --git a/src/proxy/index.js b/src/proxy/index.js deleted file mode 100644 index d4e8e3f93..000000000 --- a/src/proxy/index.js +++ /dev/null @@ -1,66 +0,0 @@ -const express = require('express'); -const bodyParser = require('body-parser'); -const http = require('http'); -const https = require('https'); -const fs = require('fs'); -const path = require('path'); -const router = require('./routes').router; -const config = require('../config'); -const db = require('../db'); -const { PluginLoader } = require('../plugin'); -const chain = require('./chain'); -const { GIT_PROXY_SERVER_PORT: proxyHttpPort } = require('../config/env').Vars; -const { GIT_PROXY_HTTPS_SERVER_PORT: proxyHttpsPort } = require('../config/env').Vars; - -const options = { - inflate: true, - limit: '100000kb', - type: '*/*', - key: fs.readFileSync(path.join(__dirname, config.getSSLKeyPath())), - cert: fs.readFileSync(path.join(__dirname, config.getSSLCertPath())), -}; - -const proxyPreparations = async () => { - const plugins = config.getPlugins(); - const pluginLoader = new PluginLoader(plugins); - await pluginLoader.load(); - chain.chainPluginLoader = pluginLoader; - // Check to see if the default repos are in the repo list - const defaultAuthorisedRepoList = config.getAuthorisedList(); - const allowedList = await db.getRepos(); - - defaultAuthorisedRepoList.forEach(async (x) => { - const found = allowedList.find((y) => y.project === x.project && x.name === y.name); - if (!found) { - await db.createRepo(x); - await db.addUserCanPush(x.name, 'admin'); - await db.addUserCanAuthorise(x.name, 'admin'); - } - }); -}; - -// just keep this async incase it needs async stuff in the future -const createApp = async () => { - const app = express(); - // Setup the proxy middleware - app.use(bodyParser.raw(options)); - app.use('/', router); - return app; -}; - -const start = async () => { - const app = await createApp(); - await proxyPreparations(); - http.createServer(options, app).listen(proxyHttpPort, () => { - console.log(`HTTP Proxy Listening on ${proxyHttpPort}`); - }); - https.createServer(options, app).listen(proxyHttpsPort, () => { - console.log(`HTTPS Proxy Listening on ${proxyHttpsPort}`); - }); - - return app; -}; - -module.exports.proxyPreparations = proxyPreparations; -module.exports.createApp = createApp; -module.exports.start = start; diff --git a/src/proxy/index.ts b/src/proxy/index.ts new file mode 100644 index 000000000..89a0977b3 --- /dev/null +++ b/src/proxy/index.ts @@ -0,0 +1,80 @@ +import express from 'express'; +import bodyParser from 'body-parser'; +import http from 'http'; +import https from 'https'; +import fs from 'fs'; +import path from 'path'; +import { router } from './routes'; +import { + getAuthorisedList, + getPlugins, + getSSLCertPath, + getSSLKeyPath +} from '../config'; +import { + addUserCanAuthorise, + addUserCanPush, + createRepo, + getRepos +} from '../db'; +import { PluginLoader } from '../plugin'; +import chain from './chain'; +import { Repo } from '../db/types'; + +const { GIT_PROXY_SERVER_PORT: proxyHttpPort, GIT_PROXY_HTTPS_SERVER_PORT: proxyHttpsPort } = + require('../config/env').serverConfig; + +const options = { + inflate: true, + limit: '100000kb', + type: '*/*', + key: fs.readFileSync(path.join(__dirname, getSSLKeyPath())), + cert: fs.readFileSync(path.join(__dirname, getSSLCertPath())), +}; + +const proxyPreparations = async () => { + const plugins = getPlugins(); + const pluginLoader = new PluginLoader(plugins); + await pluginLoader.load(); + chain.chainPluginLoader = pluginLoader; + // Check to see if the default repos are in the repo list + const defaultAuthorisedRepoList = getAuthorisedList(); + const allowedList: Repo[] = await getRepos(); + + defaultAuthorisedRepoList.forEach(async (x) => { + const found = allowedList.find((y) => y.project === x.project && x.name === y.name); + if (!found) { + await createRepo(x); + await addUserCanPush(x.name, 'admin'); + await addUserCanAuthorise(x.name, 'admin'); + } + }); +}; + +// just keep this async incase it needs async stuff in the future +const createApp = async () => { + const app = express(); + // Setup the proxy middleware + app.use(bodyParser.raw(options)); + app.use('/', router); + return app; +}; + +const start = async () => { + const app = await createApp(); + await proxyPreparations(); + http.createServer(options as any, app).listen(proxyHttpPort, () => { + console.log(`HTTP Proxy Listening on ${proxyHttpPort}`); + }); + https.createServer(options, app).listen(proxyHttpsPort, () => { + console.log(`HTTPS Proxy Listening on ${proxyHttpsPort}`); + }); + + return app; +}; + +export default { + proxyPreparations, + createApp, + start +}; diff --git a/src/proxy/processors/index.js b/src/proxy/processors/index.js deleted file mode 100644 index 9cefa0db3..000000000 --- a/src/proxy/processors/index.js +++ /dev/null @@ -1,5 +0,0 @@ -const pre = require('./pre-processor'); -const push = require('./push-action'); - -module.exports.pre = pre; -module.exports.push = push; diff --git a/src/proxy/processors/index.ts b/src/proxy/processors/index.ts new file mode 100644 index 000000000..587a83e4f --- /dev/null +++ b/src/proxy/processors/index.ts @@ -0,0 +1,4 @@ +import * as pre from './pre-processor'; +import * as push from './push-action'; + +export { pre, push }; diff --git a/src/proxy/processors/pre-processor/index.js b/src/proxy/processors/pre-processor/index.js deleted file mode 100644 index d315f735f..000000000 --- a/src/proxy/processors/pre-processor/index.js +++ /dev/null @@ -1 +0,0 @@ -exports.parseAction = require('./parseAction').exec; diff --git a/src/proxy/processors/pre-processor/index.ts b/src/proxy/processors/pre-processor/index.ts new file mode 100644 index 000000000..469c71efc --- /dev/null +++ b/src/proxy/processors/pre-processor/index.ts @@ -0,0 +1,3 @@ +import { exec } from './parseAction'; + +export const parseAction = exec; diff --git a/src/proxy/processors/pre-processor/parseAction.js b/src/proxy/processors/pre-processor/parseAction.ts similarity index 50% rename from src/proxy/processors/pre-processor/parseAction.js rename to src/proxy/processors/pre-processor/parseAction.ts index b10f0372b..ed610d9d1 100644 --- a/src/proxy/processors/pre-processor/parseAction.js +++ b/src/proxy/processors/pre-processor/parseAction.ts @@ -1,6 +1,6 @@ -const actions = require('../../actions'); +import { Action } from '../../actions'; -const exec = async (req) => { +const exec = async (req: { originalUrl: string; method: string; headers: Record }) => { const id = Date.now(); const timestamp = id; const repoName = getRepoNameFromUrl(req.originalUrl); @@ -8,30 +8,31 @@ const exec = async (req) => { let type = 'default'; - if (paths[paths.length - 1].endsWith('git-upload-pack') && req.method == 'GET') { + if (paths[paths.length - 1].endsWith('git-upload-pack') && req.method === 'GET') { type = 'pull'; } if ( - paths[paths.length - 1] == 'git-receive-pack' && - req.method == 'POST' && - req.headers['content-type'] == 'application/x-git-receive-pack-request' + paths[paths.length - 1] === 'git-receive-pack' && + req.method === 'POST' && + req.headers['content-type'] === 'application/x-git-receive-pack-request' ) { type = 'push'; } - return new actions.Action(id, type, req.method, timestamp, repoName); + + return new Action(id.toString(), type, req.method, timestamp, repoName); }; -const getRepoNameFromUrl = (url) => { +const getRepoNameFromUrl = (url: string): string => { const parts = url.split('/'); for (let i = 0, len = parts.length; i < len; i++) { const part = parts[i]; if (part.endsWith('.git')) { - const repo = `${parts[i - 1]}/${part}`; - return repo.trim(); + return `${parts[i - 1]}/${part}`.trim(); } } return 'NOT-FOUND'; }; exec.displayName = 'parseAction.exec'; -exports.exec = exec; + +export { exec }; diff --git a/src/proxy/processors/push-action/audit.js b/src/proxy/processors/push-action/audit.js deleted file mode 100644 index 8651da558..000000000 --- a/src/proxy/processors/push-action/audit.js +++ /dev/null @@ -1,12 +0,0 @@ -const db = require('../../../db'); - -const exec = async (req, action) => { - if (action.type !== 'pull') { - await db.writeAudit(action); - } - - return action; -}; - -exec.displayName = 'audit.exec'; -exports.exec = exec; diff --git a/src/proxy/processors/push-action/audit.ts b/src/proxy/processors/push-action/audit.ts new file mode 100644 index 000000000..32e556fb7 --- /dev/null +++ b/src/proxy/processors/push-action/audit.ts @@ -0,0 +1,14 @@ +import { writeAudit } from '../../../db'; +import { Action } from '../../actions'; + +const exec = async (req: any, action: Action) => { + if (action.type !== 'pull') { + await writeAudit(action); + } + + return action; +}; + +exec.displayName = 'audit.exec'; + +export { exec }; diff --git a/src/proxy/processors/push-action/blockForAuth.js b/src/proxy/processors/push-action/blockForAuth.ts similarity index 69% rename from src/proxy/processors/push-action/blockForAuth.js rename to src/proxy/processors/push-action/blockForAuth.ts index cc99d6b92..bbef7725d 100644 --- a/src/proxy/processors/push-action/blockForAuth.js +++ b/src/proxy/processors/push-action/blockForAuth.ts @@ -1,8 +1,7 @@ -const { getServiceUIURL } = require('../../../service/urls'); +import { Action, Step } from '../../actions'; +import { getServiceUIURL } from '../../../service/urls'; -const Step = require('../../actions').Step; - -const exec = async (req, action) => { +const exec = async (req: any, action: Action) => { const step = new Step('authBlock'); const url = getServiceUIURL(req); @@ -19,4 +18,5 @@ const exec = async (req, action) => { }; exec.displayName = 'blockForAuth.exec'; -exports.exec = exec; + +export { exec }; diff --git a/src/proxy/processors/push-action/checkAuthorEmails.js b/src/proxy/processors/push-action/checkAuthorEmails.ts similarity index 78% rename from src/proxy/processors/push-action/checkAuthorEmails.js rename to src/proxy/processors/push-action/checkAuthorEmails.ts index 70c973d05..a25a0e6c9 100644 --- a/src/proxy/processors/push-action/checkAuthorEmails.js +++ b/src/proxy/processors/push-action/checkAuthorEmails.ts @@ -1,9 +1,10 @@ -const Step = require('../../actions').Step; -const config = require('../../../config'); +import { Action, Step } from '../../actions'; +import { getCommitConfig } from '../../../config'; +import { Commit } from '../../actions/Action'; -const commitConfig = config.getCommitConfig(); +const commitConfig = getCommitConfig(); -function isEmailAllowed(email) { +const isEmailAllowed = (email: string): boolean => { const [emailLocal, emailDomain] = email.split('@'); console.log({ emailLocal, emailDomain }); @@ -28,13 +29,12 @@ function isEmailAllowed(email) { return true; } -// Execute if the repo is approved -const exec = async (req, action) => { +const exec = async (req: any, action: Action): Promise => { console.log({ req, action }); const step = new Step('checkAuthorEmails'); - const uniqueAuthorEmails = [...new Set(action.commitData.map((commit) => commit.authorEmail))]; + const uniqueAuthorEmails = [...new Set(action.commitData?.map((commit: Commit) => commit.authorEmail))]; console.log({ uniqueAuthorEmails }); const illegalEmails = uniqueAuthorEmails.filter((email) => !isEmailAllowed(email)); @@ -62,4 +62,5 @@ const exec = async (req, action) => { }; exec.displayName = 'checkAuthorEmails.exec'; -exports.exec = exec; + +export { exec }; diff --git a/src/proxy/processors/push-action/checkCommitMessages.js b/src/proxy/processors/push-action/checkCommitMessages.ts similarity index 77% rename from src/proxy/processors/push-action/checkCommitMessages.js rename to src/proxy/processors/push-action/checkCommitMessages.ts index 8f65933cc..577a572af 100644 --- a/src/proxy/processors/push-action/checkCommitMessages.js +++ b/src/proxy/processors/push-action/checkCommitMessages.ts @@ -1,9 +1,9 @@ -const Step = require('../../actions').Step; -const config = require('../../../config'); +import { Action, Step } from '../../actions'; +import { getCommitConfig } from '../../../config'; -const commitConfig = config.getCommitConfig(); +const commitConfig = getCommitConfig(); -function isMessageAllowed(commitMessage) { +const isMessageAllowed = (commitMessage: string): boolean => { console.log(`isMessageAllowed(${commitMessage})`); // Commit message is empty, i.e. '', null or undefined @@ -19,18 +19,18 @@ function isMessageAllowed(commitMessage) { } // Configured blocked literals - const blockedLiterals = commitConfig.message.block.literals; + const blockedLiterals: string[] = commitConfig.message.block.literals; // Configured blocked patterns - const blockedPatterns = commitConfig.message.block.patterns; + const blockedPatterns: string[] = commitConfig.message.block.patterns; // Find all instances of blocked literals in commit message... - const positiveLiterals = blockedLiterals.map((literal) => + const positiveLiterals = blockedLiterals.map((literal: string) => commitMessage.toLowerCase().includes(literal.toLowerCase()), ); // Find all instances of blocked patterns in commit message... - const positivePatterns = blockedPatterns.map((pattern) => + const positivePatterns = blockedPatterns.map((pattern: string) => commitMessage.match(new RegExp(pattern, 'gi')), ); @@ -50,12 +50,12 @@ function isMessageAllowed(commitMessage) { } // Execute if the repo is approved -const exec = async (req, action) => { +const exec = async (req: any, action: Action): Promise => { console.log({ req, action }); const step = new Step('checkCommitMessages'); - const uniqueCommitMessages = [...new Set(action.commitData.map((commit) => commit.message))]; + const uniqueCommitMessages = [...new Set(action.commitData?.map((commit) => commit.message))]; console.log({ uniqueCommitMessages }); const illegalMessages = uniqueCommitMessages.filter((message) => !isMessageAllowed(message)); @@ -83,4 +83,5 @@ const exec = async (req, action) => { }; exec.displayName = 'checkCommitMessages.exec'; -exports.exec = exec; + +export { exec }; diff --git a/src/proxy/processors/push-action/checkIfWaitingAuth.js b/src/proxy/processors/push-action/checkIfWaitingAuth.ts similarity index 63% rename from src/proxy/processors/push-action/checkIfWaitingAuth.js rename to src/proxy/processors/push-action/checkIfWaitingAuth.ts index f671cd082..baedb0df3 100644 --- a/src/proxy/processors/push-action/checkIfWaitingAuth.js +++ b/src/proxy/processors/push-action/checkIfWaitingAuth.ts @@ -1,11 +1,11 @@ -const Step = require('../../actions').Step; -const data = require('../../../db'); +import { Action, Step } from '../../actions'; +import { getPush } from '../../../db'; // Execute function -const exec = async (req, action) => { +const exec = async (req: any, action: Action): Promise => { const step = new Step('checkIfWaitingAuth'); try { - const existingAction = await data.getPush(action.id); + const existingAction = await getPush(action.id); if (existingAction) { if (!action.error) { if (existingAction.authorised) { @@ -14,7 +14,7 @@ const exec = async (req, action) => { } } } - } catch (e) { + } catch (e: any) { step.setError(e.toString('utf-8')); throw e; } finally { @@ -24,4 +24,5 @@ const exec = async (req, action) => { }; exec.displayName = 'checkIfWaitingAuth.exec'; -exports.exec = exec; + +export { exec }; diff --git a/src/proxy/processors/push-action/checkRepoInAuthorisedList.js b/src/proxy/processors/push-action/checkRepoInAuthorisedList.ts similarity index 74% rename from src/proxy/processors/push-action/checkRepoInAuthorisedList.js rename to src/proxy/processors/push-action/checkRepoInAuthorisedList.ts index e43a1d202..9560dc58d 100644 --- a/src/proxy/processors/push-action/checkRepoInAuthorisedList.js +++ b/src/proxy/processors/push-action/checkRepoInAuthorisedList.ts @@ -1,14 +1,19 @@ -const Step = require('../../actions').Step; -const db = require('../../../db'); +import { Action, Step } from '../../actions'; +import { getRepos } from '../../../db'; +import { Repo } from '../../../db/types'; // Execute if the repo is approved -const exec = async (req, action, authorisedList = db.getRepos) => { +const exec = async ( + req: any, + action: Action, + authorisedList: () => Promise = getRepos, +): Promise => { const step = new Step('checkRepoInAuthorisedList'); const list = await authorisedList(); console.log(list); - const found = list.find((x) => { + const found = list.find((x: Repo) => { const targetName = action.repo.replace('.git', '').toLowerCase(); const allowedName = `${x.project}/${x.name}`.replace('.git', '').toLowerCase(); console.log(`${targetName} = ${allowedName}`); @@ -34,4 +39,5 @@ const exec = async (req, action, authorisedList = db.getRepos) => { }; exec.displayName = 'checkRepoInAuthorisedList.exec'; -exports.exec = exec; + +export { exec }; diff --git a/src/proxy/processors/push-action/checkUserPushPermission.js b/src/proxy/processors/push-action/checkUserPushPermission.ts similarity index 67% rename from src/proxy/processors/push-action/checkUserPushPermission.js rename to src/proxy/processors/push-action/checkUserPushPermission.ts index 3daff8250..c712693e5 100644 --- a/src/proxy/processors/push-action/checkUserPushPermission.js +++ b/src/proxy/processors/push-action/checkUserPushPermission.ts @@ -1,8 +1,8 @@ -const Step = require('../../actions').Step; -const db = require('../../../db'); +import { Action, Step } from '../../actions'; +import { getUsers, isUserPushAllowed } from '../../../db'; // Execute if the repo is approved -const exec = async (req, action) => { +const exec = async (req: any, action: Action): Promise => { const step = new Step('checkUserPushPermission'); const repoName = action.repo.split('/')[1].replace('.git', ''); @@ -10,13 +10,13 @@ const exec = async (req, action) => { let user = action.user; // Find the user associated with this Git Account - const list = await db.getUsers({ gitAccount: action.user }); + const list = await getUsers({ gitAccount: action.user }); - console.log(JSON.stringify(list)); + console.log(`Users for this git account: ${JSON.stringify(list)}`); if (list.length == 1) { user = list[0].username; - isUserAllowed = await db.isUserPushAllowed(repoName, user); + isUserAllowed = await isUserPushAllowed(repoName, user); } console.log(`User ${user} permission on Repo ${repoName} : ${isUserAllowed}`); @@ -30,8 +30,8 @@ const exec = async (req, action) => { step.setError( `Rejecting push as user ${action.user} ` + - `is not allowed to push on repo ` + - `${action.repo}`, + `is not allowed to push on repo ` + + `${action.repo}`, ); action.addStep(step); return action; @@ -43,4 +43,5 @@ const exec = async (req, action) => { }; exec.displayName = 'checkUserPushPermission.exec'; -exports.exec = exec; + +export { exec }; diff --git a/src/proxy/processors/push-action/clearBareClone.js b/src/proxy/processors/push-action/clearBareClone.ts similarity index 69% rename from src/proxy/processors/push-action/clearBareClone.js rename to src/proxy/processors/push-action/clearBareClone.ts index dfda43eb8..91f7f5b22 100644 --- a/src/proxy/processors/push-action/clearBareClone.js +++ b/src/proxy/processors/push-action/clearBareClone.ts @@ -1,7 +1,7 @@ -const Step = require('../../actions').Step; -const fs = require('node:fs'); +import { Action, Step } from '../../actions'; +import fs from 'node:fs'; -const exec = async (req, action) => { +const exec = async (req: any, action: Action): Promise => { const step = new Step('clearBareClone'); // Recursively remove the contents of ./.remote and ignore exceptions @@ -17,4 +17,5 @@ const exec = async (req, action) => { }; exec.displayName = 'clearBareClone.exec'; -exports.exec = exec; + +export { exec }; diff --git a/src/proxy/processors/push-action/getDiff.js b/src/proxy/processors/push-action/getDiff.ts similarity index 78% rename from src/proxy/processors/push-action/getDiff.js rename to src/proxy/processors/push-action/getDiff.ts index 0a91161f4..89ac0afbd 100644 --- a/src/proxy/processors/push-action/getDiff.js +++ b/src/proxy/processors/push-action/getDiff.ts @@ -1,8 +1,7 @@ -const Step = require('../../actions').Step; -const simpleGit = require('simple-git') +import { Action, Step } from '../../actions'; +import simpleGit from 'simple-git'; - -const exec = async (req, action) => { +const exec = async (req: any, action: Action): Promise => { const step = new Step('diff'); try { @@ -11,6 +10,10 @@ const exec = async (req, action) => { // https://stackoverflow.com/questions/40883798/how-to-get-git-diff-of-the-first-commit let commitFrom = `4b825dc642cb6eb9a060e54bf8d69288fbee4904`; + if (!action.commitData) { + throw new Error('No commit data found'); + } + if (action.commitFrom === '0000000000000000000000000000000000000000') { if (action.commitData[0].parent !== '0000000000000000000000000000000000000000') { commitFrom = `${action.commitData[action.commitData.length - 1].parent}`; @@ -24,7 +27,7 @@ const exec = async (req, action) => { const diff = await git.diff([revisionRange]); step.log(diff); step.setContent(diff); - } catch (e) { + } catch (e: any) { step.setError(e.toString('utf-8')); } finally { action.addStep(step); @@ -33,4 +36,5 @@ const exec = async (req, action) => { }; exec.displayName = 'getDiff.exec'; -exports.exec = exec; + +export { exec }; diff --git a/src/proxy/processors/push-action/index.js b/src/proxy/processors/push-action/index.js deleted file mode 100644 index 3d6031245..000000000 --- a/src/proxy/processors/push-action/index.js +++ /dev/null @@ -1,14 +0,0 @@ -exports.parsePush = require('./parsePush').exec; -exports.preReceive = require('./preReceive').exec; -exports.checkRepoInAuthorisedList = require('./checkRepoInAuthorisedList').exec; -exports.audit = require('./audit').exec; -exports.pullRemote = require('./pullRemote').exec; -exports.writePack = require('./writePack').exec; -exports.getDiff = require('./getDiff').exec; -exports.scanDiff = require('./scanDiff').exec; -exports.blockForAuth = require('./blockForAuth').exec; -exports.checkIfWaitingAuth = require('./checkIfWaitingAuth').exec; -exports.checkCommitMessages = require('./checkCommitMessages').exec; -exports.checkAuthorEmails = require('./checkAuthorEmails').exec; -exports.checkUserPushPermission = require('./checkUserPushPermission').exec; -exports.clearBareClone = require('./clearBareClone').exec; diff --git a/src/proxy/processors/push-action/index.ts b/src/proxy/processors/push-action/index.ts new file mode 100644 index 000000000..9fc2065f7 --- /dev/null +++ b/src/proxy/processors/push-action/index.ts @@ -0,0 +1,31 @@ +import { exec as parsePush } from './parsePush'; +import { exec as preReceive } from './preReceive'; +import { exec as checkRepoInAuthorisedList } from './checkRepoInAuthorisedList'; +import { exec as audit } from './audit'; +import { exec as pullRemote } from './pullRemote'; +import { exec as writePack } from './writePack'; +import { exec as getDiff } from './getDiff'; +import { exec as scanDiff } from './scanDiff'; +import { exec as blockForAuth } from './blockForAuth'; +import { exec as checkIfWaitingAuth } from './checkIfWaitingAuth'; +import { exec as checkCommitMessages } from './checkCommitMessages'; +import { exec as checkAuthorEmails } from './checkAuthorEmails'; +import { exec as checkUserPushPermission } from './checkUserPushPermission'; +import { exec as clearBareClone } from './clearBareClone'; + +export { + parsePush, + preReceive, + checkRepoInAuthorisedList, + audit, + pullRemote, + writePack, + getDiff, + scanDiff, + blockForAuth, + checkIfWaitingAuth, + checkCommitMessages, + checkAuthorEmails, + checkUserPushPermission, + clearBareClone, +}; diff --git a/src/proxy/processors/push-action/parsePush.js b/src/proxy/processors/push-action/parsePush.ts similarity index 72% rename from src/proxy/processors/push-action/parsePush.js rename to src/proxy/processors/push-action/parsePush.ts index d8ec57f67..42adcb48e 100644 --- a/src/proxy/processors/push-action/parsePush.js +++ b/src/proxy/processors/push-action/parsePush.ts @@ -1,28 +1,35 @@ -const zlib = require('zlib'); -const fs = require('fs'); -const lod = require('lodash'); -const BitMask = require('bit-mask'); -const Step = require('../../actions').Step; -const dir = './.tmp/'; +import { Action, Step } from '../../actions'; +import zlib from 'zlib'; +import fs from 'fs'; +import path from 'path'; +import lod from 'lodash'; +import { CommitContent } from '../types'; +const BitMask = require('bit-mask') as any; + +const dir = path.resolve(__dirname, './.tmp'); if (!fs.existsSync(dir)) { - fs.mkdirSync(dir); + fs.mkdirSync(dir, { recursive: true }); } -const exec = async (req, action) => { +async function exec(req: any, action: Action): Promise { const step = new Step('parsePackFile'); try { - const messageParts = req.rawBody.split(' '); + if (!req.body || req.body.length === 0) { + throw new Error('No body found in request'); + } + const messageParts = req.body.toString('utf8').split(' '); + action.branch = messageParts[2].trim().replace('\u0000', ''); action.setCommit(messageParts[0].substr(4), messageParts[1]); const index = req.body.lastIndexOf('PACK'); const buf = req.body.slice(index); const [meta, contentBuff] = getPackMeta(buf); - const contents = getContents(contentBuff, meta.entries); + const contents = getContents(contentBuff as any, meta.entries as number); - action.commitData = getCommitData(contents); + action.commitData = getCommitData(contents as any); if (action.commitFrom === '0000000000000000000000000000000000000000') { action.commitFrom = action.commitData[action.commitData.length - 1].parent; @@ -35,7 +42,7 @@ const exec = async (req, action) => { step.content = { meta: meta, }; - } catch (e) { + } catch (e: any) { step.setError( `Unable to parse push. Please contact an administrator for support: ${e.toString('utf-8')}`, ); @@ -45,7 +52,7 @@ const exec = async (req, action) => { return action; }; -const getCommitData = (contents) => { +const getCommitData = (contents: CommitContent[]) => { console.log({ contents }); return lod .chain(contents) @@ -59,9 +66,13 @@ const getCommitData = (contents) => { const parts = formattedContent.filter((part) => part.length > 0); console.log({ parts }); + if (!parts || parts.length < 5) { + throw new Error('Invalid commit data'); + } + const tree = parts .find((t) => t.split(' ')[0] === 'tree') - .replace('tree', '') + ?.replace('tree', '') .trim(); console.log({ tree }); @@ -75,13 +86,13 @@ const getCommitData = (contents) => { const author = parts .find((t) => t.split(' ')[0] === 'author') - .replace('author', '') + ?.replace('author', '') .trim(); console.log({ author }); const committer = parts .find((t) => t.split(' ')[0] === 'committer') - .replace('committer', '') + ?.replace('committer', '') .trim(); console.log({ committer }); @@ -93,22 +104,26 @@ const getCommitData = (contents) => { .join(' '); console.log({ message }); - const commitTimestamp = committer.split(' ').reverse()[1]; + const commitTimestamp = committer?.split(' ').reverse()[1]; console.log({ commitTimestamp }); - const authorEmail = author.split(' ').reverse()[2].slice(1, -1); + const authorEmail = author?.split(' ').reverse()[2].slice(1, -1); console.log({ authorEmail }); console.log({ tree, parent, - author: author.split('<')[0].trim(), - committer: committer.split('<')[0].trim(), + author: author?.split('<')[0].trim(), + committer: committer?.split('<')[0].trim(), commitTimestamp, message, authorEmail, }); + if (!tree || !parent || !author || !committer || !commitTimestamp || !message || !authorEmail) { + throw new Error('Invalid commit data'); + } + return { tree, parent, @@ -116,13 +131,13 @@ const getCommitData = (contents) => { committer: committer.split('<')[0].trim(), commitTimestamp, message, - authorEmail, + authorEmail: authorEmail, }; }) .value(); }; -const getPackMeta = (buffer) => { +const getPackMeta = (buffer: Buffer) => { const sig = buffer.slice(0, 4).toString('utf-8'); const version = buffer.readUIntBE(4, 4); const entries = buffer.readUIntBE(8, 4); @@ -136,13 +151,13 @@ const getPackMeta = (buffer) => { return [meta, buffer.slice(12)]; }; -const getContents = (buffer, entries) => { +const getContents = (buffer: Buffer | CommitContent[], entries: number) => { const contents = []; for (let i = 0; i < entries; i++) { try { - const [content, nextBuffer] = getContent(i, buffer); - buffer = nextBuffer; + const [content, nextBuffer] = getContent(i, buffer as Buffer); + buffer = nextBuffer as Buffer; contents.push(content); } catch (e) { console.log(e); @@ -151,7 +166,7 @@ const getContents = (buffer, entries) => { return contents; }; -const getInt = (bits) => { +const getInt = (bits: boolean[]) => { let strBits = ''; // eslint-disable-next-line guard-for-in @@ -162,7 +177,7 @@ const getInt = (bits) => { return parseInt(strBits, 2); }; -const getContent = (item, buffer) => { +const getContent = (item: number, buffer: Buffer) => { // FIRST byte contains the type and some of the size of the file // a MORE flag -8th byte tells us if there is a subsequent byte // which holds the file size @@ -175,7 +190,7 @@ const getContent = (item, buffer) => { const type = getInt([m.getBit(4), m.getBit(5), m.getBit(6)]); // Object IDs if this is a deltatfied blob - let objectRef = null; + let objectRef: string | null = null; // If we have a more flag get the next // 8 bytes @@ -199,7 +214,7 @@ const getContent = (item, buffer) => { } // NOTE Size is the unziped size, not the zipped size - size = getInt(size); + const intSize = getInt(size); // Deltafied objectives have a 20 byte identifer if (type == 7 || type == 6) { @@ -216,19 +231,19 @@ const getContent = (item, buffer) => { item: item, value: byte, type: type, - size: size, + size: intSize, deflatedSize: deflatedSize, objectRef: objectRef, content: content, }; // Move on by the zipped content size. - const nextBuffer = contentBuffer.slice(deflatedSize); + const nextBuffer = contentBuffer.slice(deflatedSize as number); return [result, nextBuffer]; }; -const unpack = (buf) => { +const unpack = (buf: Buffer) => { // Unzip the content const inflated = zlib.inflateSync(buf); @@ -240,6 +255,9 @@ const unpack = (buf) => { }; exec.displayName = 'parsePush.exec'; -exports.exec = exec; -exports.getPackMeta = getPackMeta; -exports.unpack = unpack; + +export { + exec, + getPackMeta, + unpack +}; diff --git a/src/proxy/processors/push-action/preReceive.js b/src/proxy/processors/push-action/preReceive.ts similarity index 83% rename from src/proxy/processors/push-action/preReceive.js rename to src/proxy/processors/push-action/preReceive.ts index 8634a6f0b..1a8b23d89 100644 --- a/src/proxy/processors/push-action/preReceive.js +++ b/src/proxy/processors/push-action/preReceive.ts @@ -1,13 +1,17 @@ -const fs = require('fs'); -const path = require('path'); -const Step = require('../../actions').Step; -const { spawnSync } = require('child_process'); +import fs from 'fs'; +import path from 'path'; +import { Action, Step } from '../../actions'; +import { spawnSync } from 'child_process'; -const sanitizeInput = (_req, action) => { +const sanitizeInput = (_req: any, action: Action): string => { return `${action.commitFrom} ${action.commitTo} ${action.branch} \n`; }; -const exec = async (req, action, hookFilePath = './hooks/pre-receive.sh') => { +const exec = async ( + req: any, + action: Action, + hookFilePath: string = './hooks/pre-receive.sh' +): Promise => { const step = new Step('executeExternalPreReceiveHook'); let stderrTrimmed = ''; @@ -58,7 +62,7 @@ const exec = async (req, action, hookFilePath = './hooks/pre-receive.sh') => { action.addStep(step); } return action; - } catch (error) { + } catch (error: any) { step.error = true; step.log('Push failed, pre-receive hook returned an error.'); step.setError(`Hook execution error: ${stderrTrimmed || error.message}`); @@ -68,4 +72,5 @@ const exec = async (req, action, hookFilePath = './hooks/pre-receive.sh') => { }; exec.displayName = 'executeExternalPreReceiveHook.exec'; -exports.exec = exec; + +export { exec }; diff --git a/src/proxy/processors/push-action/pullRemote.js b/src/proxy/processors/push-action/pullRemote.ts similarity index 74% rename from src/proxy/processors/push-action/pullRemote.js rename to src/proxy/processors/push-action/pullRemote.ts index cafb4fc7f..c7559643f 100644 --- a/src/proxy/processors/push-action/pullRemote.js +++ b/src/proxy/processors/push-action/pullRemote.ts @@ -1,10 +1,11 @@ -const Step = require('../../actions').Step; -const fs = require('fs'); +import { Action, Step } from '../../actions'; +import fs from 'fs' +import git from 'isomorphic-git'; +import gitHttpClient from 'isomorphic-git/http/node'; + const dir = './.remote'; -const git = require('isomorphic-git'); -const gitHttpClient = require('isomorphic-git/http/node'); -const exec = async (req, action) => { +const exec = async (req: any, action: Action): Promise => { const step = new Step('pullRemote'); try { @@ -17,7 +18,7 @@ const exec = async (req, action) => { } if (!fs.existsSync(action.proxyGitPath)) { - fs.mkdirSync(action.proxyGitPath, '0755', true); + fs.mkdirSync(action.proxyGitPath, 0o755); } const cmd = `git clone ${action.url}`; @@ -40,11 +41,11 @@ const exec = async (req, action) => { dir: `${action.proxyGitPath}/${action.repoName}`, }); - console.log('Clone Success: ', action.url); + console.log('Clone Success: ', action.url); step.log(`Completed ${cmd}`); step.setContent(`Completed ${cmd}`); - } catch (e) { + } catch (e: any) { step.setError(e.toString('utf-8')); throw e; } finally { @@ -54,4 +55,5 @@ const exec = async (req, action) => { }; exec.displayName = 'pullRemote.exec'; -exports.exec = exec; + +export { exec }; diff --git a/src/proxy/processors/push-action/scanDiff.js b/src/proxy/processors/push-action/scanDiff.ts similarity index 77% rename from src/proxy/processors/push-action/scanDiff.js rename to src/proxy/processors/push-action/scanDiff.ts index 61fa529c7..296c8b404 100644 --- a/src/proxy/processors/push-action/scanDiff.js +++ b/src/proxy/processors/push-action/scanDiff.ts @@ -1,9 +1,9 @@ -const Step = require('../../actions').Step; -const config = require('../../../config'); -const parseDiff = require('parse-diff') +import { Action, Step } from '../../actions'; +import { getCommitConfig, getPrivateOrganizations } from '../../../config'; +import parseDiff, { File } from 'parse-diff'; -const commitConfig = config.getCommitConfig(); -const privateOrganizations = config.getPrivateOrganizations(); +const commitConfig = getCommitConfig(); +const privateOrganizations = getPrivateOrganizations(); const BLOCK_TYPE = { LITERAL: 'Offending Literal', @@ -11,8 +11,28 @@ const BLOCK_TYPE = { PROVIDER: 'PROVIDER' } +type CombinedMatch = { + type: string; + match: RegExp; +} + +type RawMatch = { + type: string; + literal: string; + file?: string; + lines: number[]; + content: string; +} + +type Match = { + type: string; + literal: string; + file?: string; + lines: string; + content: string; +} -const getDiffViolations = (diff, organization) => { +const getDiffViolations = (diff: string, organization: string): Match[] | string | null => { // Commit diff is empty, i.e. '', null or undefined if (!diff) { console.log('No commit diff...'); @@ -28,7 +48,6 @@ const getDiffViolations = (diff, organization) => { const parsedDiff = parseDiff(diff); const combinedMatches = combineMatches(organization); - const res = collectMatches(parsedDiff, combinedMatches); // Diff matches configured block pattern(s) if (res.length > 0) { @@ -40,16 +59,15 @@ const getDiffViolations = (diff, organization) => { return null; }; -const combineMatches = (organization) => { - +const combineMatches = (organization: string) => { // Configured blocked literals - const blockedLiterals = commitConfig.diff.block.literals; + const blockedLiterals: string[] = commitConfig.diff.block.literals; // Configured blocked patterns - const blockedPatterns = commitConfig.diff.block.patterns; + const blockedPatterns: string[] = commitConfig.diff.block.patterns; // Configured blocked providers - const blockedProviders = organization && privateOrganizations.includes(organization) ? [] : + const blockedProviders: [string, string][] = organization && privateOrganizations.includes(organization) ? [] : Object.entries(commitConfig.diff.block.providers); // Combine all matches (literals, paterns) @@ -70,15 +88,19 @@ const combineMatches = (organization) => { return combinedMatches; } -const collectMatches = (parsedDiff, combinedMatches) => { - const allMatches = {}; +const collectMatches = ( + parsedDiff: File[], + combinedMatches: CombinedMatch[] +): Match[] => { + const allMatches: Record = {}; parsedDiff.forEach(file => { const fileName = file.to || file.from; console.log("CHANGE", file.chunks) file.chunks.forEach(chunk => { chunk.changes.forEach(change => { - if (change.add) { + console.log("CHANGE", change) + if (change.type === 'add') { // store line number const lineNumber = change.ln; // Iterate through each match types - literal, patterns, providers @@ -117,11 +139,10 @@ const collectMatches = (parsedDiff, combinedMatches) => { lines: match.lines.join(',') // join the line numbers into a comma-separated string })) - console.log("RESULT", result) return result; } -const formatMatches = (matches) => { +const formatMatches = (matches: Match[]) => { return matches.map((match, index) => { return `---------------------------------- #${index + 1} ${match.type} ------------------------------ Policy Exception Type: ${match.type} @@ -131,7 +152,7 @@ const formatMatches = (matches) => { }); } -const exec = async (req, action) => { +const exec = async (req: any, action: Action): Promise => { const step = new Step('scanDiff'); const { steps, commitFrom, commitTo } = action; @@ -158,7 +179,6 @@ const exec = async (req, action) => { errorMsg.join('\n') ); - action.addStep(step); return action; } @@ -168,4 +188,5 @@ const exec = async (req, action) => { }; exec.displayName = 'scanDiff.exec'; -exports.exec = exec; + +export { exec }; diff --git a/src/proxy/processors/push-action/writePack.js b/src/proxy/processors/push-action/writePack.ts similarity index 74% rename from src/proxy/processors/push-action/writePack.js rename to src/proxy/processors/push-action/writePack.ts index a2a35c4e8..fbcc2fddb 100644 --- a/src/proxy/processors/push-action/writePack.js +++ b/src/proxy/processors/push-action/writePack.ts @@ -1,7 +1,7 @@ -const spawnSync = require('child_process').spawnSync; -const Step = require('../../actions').Step; +import { Action, Step } from '../../actions'; +import { spawnSync } from 'child_process'; -const exec = async (req, action) => { +const exec = async (req: any, action: Action) => { const step = new Step('writePack'); try { const cmd = `git receive-pack ${action.repoName}`; @@ -15,7 +15,7 @@ const exec = async (req, action) => { step.log(content); step.setContent(content); - } catch (e) { + } catch (e: any) { step.setError(e.toString('utf-8')); throw e; } finally { @@ -25,4 +25,5 @@ const exec = async (req, action) => { }; exec.displayName = 'writePack.exec'; -exports.exec = exec; + +export { exec }; diff --git a/src/proxy/processors/types.ts b/src/proxy/processors/types.ts new file mode 100644 index 000000000..bb267ce90 --- /dev/null +++ b/src/proxy/processors/types.ts @@ -0,0 +1,20 @@ +import { Action } from "../actions"; + +export interface Processor { + exec(req: any, action: Action): Promise; + metadata: ProcessorMetadata; +} + +export interface ProcessorMetadata { + displayName: string; +} + +export type CommitContent = { + item: number; + value: number; + type: number; + size: number; + deflatedSize: number; + objectRef: any; + content: string; +} diff --git a/src/proxy/routes/index.js b/src/proxy/routes/index.ts similarity index 84% rename from src/proxy/routes/index.js rename to src/proxy/routes/index.ts index 1ef84bcb5..c49853376 100644 --- a/src/proxy/routes/index.js +++ b/src/proxy/routes/index.ts @@ -1,8 +1,10 @@ -const express = require('express'); -const proxy = require('express-http-proxy'); -const router = new express.Router(); -const { executeChain } = require('../chain'); -const config = require('../../config'); +import { Router } from 'express'; +import proxy from 'express-http-proxy'; +import { executeChain } from '../chain'; +import { getProxyUrl } from '../../config'; + +// eslint-disable-next-line new-cap +const router = Router(); /** * For a given Git HTTP request destined for a GitHub repo, @@ -10,7 +12,7 @@ const config = require('../../config'); * @param {string} url URL path of the request * @return {string} Modified path which removes the {owner}/{repo} parts */ -const stripGitHubFromGitPath = (url) => { +const stripGitHubFromGitPath = (url: string): string | undefined => { const parts = url.split('/'); // url = '/{owner}/{repo}.git/{git-path}' // url.split('/') = ['', '{owner}', '{repo}.git', '{git-path}'] @@ -30,7 +32,7 @@ const stripGitHubFromGitPath = (url) => { * @param {*} headers Request headers (TODO: Fix JSDoc linting and refer to node:http.IncomingHttpHeaders) * @return {boolean} If true, this is a valid and expected git request. Otherwise, false. */ -const validGitRequest = (url, headers) => { +const validGitRequest = (url: string, headers: any): boolean => { const { 'user-agent': agent, accept } = headers; if (['/info/refs?service=git-upload-pack', '/info/refs?service=git-receive-pack'].includes(url)) { // https://www.git-scm.com/docs/http-protocol#_discovering_references @@ -47,7 +49,7 @@ const validGitRequest = (url, headers) => { router.use( '/', - proxy(config.getProxyUrl(), { + proxy(getProxyUrl(), { preserveHostHdr: false, filter: async function (req, res) { try { @@ -59,9 +61,6 @@ router.use( res.status(400).send('Invalid request received'); return false; } - if (req.body && req.body.length) { - req.rawBody = req.body.toString('utf8'); - } const action = await executeChain(req, res); console.log('action processed'); @@ -76,14 +75,14 @@ router.use( res.set('x-frame-options', 'DENY'); res.set('connection', 'close'); - let message; + let message = ''; if (action.error) { - message = action.errorMessage; + message = action.errorMessage!; console.error(message); } if (action.blocked) { - message = action.blockedMessage; + message = action.blockedMessage!; } const packetMessage = handleMessage(message); @@ -102,11 +101,11 @@ router.use( } }, proxyReqPathResolver: (req) => { - const url = config.getProxyUrl() + req.originalUrl; + const url = getProxyUrl() + req.originalUrl; console.log('Sending request to ' + url); return url; }, - proxyReqOptDecorator: function (proxyReqOpts, srcReq) { + proxyReqOptDecorator: function (proxyReqOpts) { return proxyReqOpts; }, @@ -124,7 +123,7 @@ router.use( }), ); -const handleMessage = (message) => { +const handleMessage = (message: string): string => { const errorMessage = `\t${message}`; const len = 6 + new TextEncoder().encode(errorMessage).length; @@ -133,7 +132,7 @@ const handleMessage = (message) => { return packetMessage; }; -module.exports = { +export { router, handleMessage, validGitRequest, diff --git a/src/service/index.js b/src/service/index.js index 39126a37c..d384fcd6e 100644 --- a/src/service/index.js +++ b/src/service/index.js @@ -14,7 +14,7 @@ const limiter = rateLimit({ max: 100, // limit each IP to 100 requests per windowMs }); -const { GIT_PROXY_UI_PORT: uiPort } = require('../config/env').Vars; +const { GIT_PROXY_UI_PORT: uiPort } = require('../config/env').serverConfig; const _httpServer = http.createServer(app); diff --git a/src/service/routes/auth.js b/src/service/routes/auth.js index e433d7ad8..92cd82e39 100644 --- a/src/service/routes/auth.js +++ b/src/service/routes/auth.js @@ -122,7 +122,7 @@ router.post('/gitAccount', async (req, res) => { user.gitAccount = req.body.gitAccount; db.updateUser(user); res.status(200).end(); - } catch (e) { + } catch { res .status(500) .send({ diff --git a/src/service/urls.js b/src/service/urls.js index 4e22a4327..2d1a60de9 100644 --- a/src/service/urls.js +++ b/src/service/urls.js @@ -1,5 +1,5 @@ const { GIT_PROXY_SERVER_PORT: PROXY_HTTP_PORT, GIT_PROXY_UI_PORT: UI_PORT } = - require('../config/env').Vars; + require('../config/env').serverConfig; const config = require('../config'); module.exports = { diff --git a/src/ui/components/Footer/Footer.jsx b/src/ui/components/Footer/Footer.jsx index 951abde8c..7cd9933f0 100644 --- a/src/ui/components/Footer/Footer.jsx +++ b/src/ui/components/Footer/Footer.jsx @@ -7,7 +7,7 @@ import { MarkGithubIcon } from '@primer/octicons-react'; const useStyles = makeStyles(styles); -export default function Footer(props) { +export default function Footer() { const classes = useStyles(); return (