diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 000000000..5eedb4a47
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1 @@
+src/service/generatedRoutes.ts
diff --git a/eslint.config.mjs b/eslint.config.mjs
index b074b622f..9f98185de 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -52,6 +52,7 @@ export default defineConfig(
// generated files we don't control
'**/package-lock.json',
'src/config/generated/**',
+ 'src/service/generatedRoutes.ts',
// has it's own eslint
'experimental/license-inventory',
// vendored code we're not changing
diff --git a/package-lock.json b/package-lock.json
index 37695ddd7..32496d6d4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -54,6 +54,7 @@
"react-router-dom": "6.30.3",
"simple-git": "^3.30.0",
"ssh2": "~1.17.0",
+ "tsoa": "^7.0.0-alpha.0",
"uuid": "^13.0.0",
"validator": "^13.15.26",
"yargs": "^17.7.2"
@@ -1771,9 +1772,9 @@
}
},
"node_modules/@cypress/request": {
- "version": "3.0.10",
- "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.10.tgz",
- "integrity": "sha512-hauBrOdvu08vOsagkZ/Aju5XuiZx6ldsLfByg1htFeldhex+PeMrYauANzFsMJeAA0+dyPLbDoX2OYuvVoLDkQ==",
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@cypress/request/-/request-4.0.1.tgz",
+ "integrity": "sha512-y20e+e6dFYkOUUJLVUZTsJRuTiXZaUQ32WD+R/ux/HBybbTx4ge7cNINcua0pU8+SNkKuRbOF12mBmzuzM8n5w==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@@ -1790,22 +1791,13 @@
"json-stringify-safe": "~5.0.1",
"mime-types": "~2.1.19",
"performance-now": "^2.1.0",
- "qs": "~6.14.1",
+ "qs": "^6.15.2",
"safe-buffer": "^5.1.2",
"tough-cookie": "^5.0.0",
- "tunnel-agent": "^0.6.0",
- "uuid": "^8.3.2"
+ "tunnel-agent": "^0.6.0"
},
"engines": {
- "node": ">= 6"
- }
- },
- "node_modules/@cypress/request/node_modules/uuid": {
- "version": "8.3.2",
- "dev": true,
- "license": "MIT",
- "bin": {
- "uuid": "dist/bin/uuid"
+ "node": ">= 14.17.0"
}
},
"node_modules/@cypress/xvfb": {
@@ -2548,6 +2540,322 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@hapi/accept": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/@hapi/accept/-/accept-6.0.3.tgz",
+ "integrity": "sha512-p72f9k56EuF0n3MwlBNThyVE5PXX40g+aQh+C/xbKrfzahM2Oispv3AXmOIU51t3j77zay1qrX7IIziZXspMlw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@hapi/boom": "^10.0.1",
+ "@hapi/hoek": "^11.0.2"
+ }
+ },
+ "node_modules/@hapi/ammo": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/@hapi/ammo/-/ammo-6.0.1.tgz",
+ "integrity": "sha512-pmL+nPod4g58kXrMcsGLp05O2jF4P2Q3GiL8qYV7nKYEh3cGf+rV4P5Jyi2Uq0agGhVU63GtaSAfBEZOlrJn9w==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@hapi/hoek": "^11.0.2"
+ }
+ },
+ "node_modules/@hapi/b64": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/@hapi/b64/-/b64-6.0.1.tgz",
+ "integrity": "sha512-ZvjX4JQReUmBheeCq+S9YavcnMMHWqx3S0jHNXWIM1kQDxB9cyfSycpVvjfrKcIS8Mh5N3hmu/YKo4Iag9g2Kw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@hapi/hoek": "^11.0.2"
+ }
+ },
+ "node_modules/@hapi/boom": {
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/@hapi/boom/-/boom-10.0.1.tgz",
+ "integrity": "sha512-ERcCZaEjdH3OgSJlyjVk8pHIFeus91CjKP3v+MpgBNp5IvGzP2l/bRiD78nqYcKPaZdbKkK5vDBVPd2ohHBlsA==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@hapi/hoek": "^11.0.2"
+ }
+ },
+ "node_modules/@hapi/bounce": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@hapi/bounce/-/bounce-3.0.2.tgz",
+ "integrity": "sha512-d0XmlTi3H9HFDHhQLjg4F4auL1EY3Wqj7j7/hGDhFFe6xAbnm3qiGrXeT93zZnPH8gH+SKAFYiRzu26xkXcH3g==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@hapi/boom": "^10.0.1",
+ "@hapi/hoek": "^11.0.2"
+ }
+ },
+ "node_modules/@hapi/bourne": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@hapi/bourne/-/bourne-3.0.0.tgz",
+ "integrity": "sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@hapi/call": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/@hapi/call/-/call-9.0.1.tgz",
+ "integrity": "sha512-uPojQRqEL1GRZR4xXPqcLMujQGaEpyVPRyBlD8Pp5rqgIwLhtveF9PkixiKru2THXvuN8mUrLeet5fqxKAAMGg==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@hapi/boom": "^10.0.1",
+ "@hapi/hoek": "^11.0.2"
+ }
+ },
+ "node_modules/@hapi/catbox": {
+ "version": "12.1.1",
+ "resolved": "https://registry.npmjs.org/@hapi/catbox/-/catbox-12.1.1.tgz",
+ "integrity": "sha512-hDqYB1J+R0HtZg4iPH3LEnldoaBsar6bYp0EonBmNQ9t5CO+1CqgCul2ZtFveW1ReA5SQuze9GPSU7/aecERhw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@hapi/boom": "^10.0.1",
+ "@hapi/hoek": "^11.0.2",
+ "@hapi/podium": "^5.0.0",
+ "@hapi/validate": "^2.0.1"
+ }
+ },
+ "node_modules/@hapi/catbox-memory": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/@hapi/catbox-memory/-/catbox-memory-6.0.2.tgz",
+ "integrity": "sha512-H1l4ugoFW/ZRkqeFrIo8p1rWN0PA4MDTfu4JmcoNDvnY975o29mqoZblqFTotxNHlEkMPpIiIBJTV+Mbi+aF0g==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@hapi/boom": "^10.0.1",
+ "@hapi/hoek": "^11.0.2"
+ }
+ },
+ "node_modules/@hapi/content": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/@hapi/content/-/content-6.0.2.tgz",
+ "integrity": "sha512-OKyCOTjNR1hftwSjk9ueyAQTw8AwapvzBrPIWMGn39vhR5PmqLdYFmLc35bsSBye7gSMnlkXfc679bUdMIcRyQ==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@hapi/boom": "^10.0.0"
+ }
+ },
+ "node_modules/@hapi/cryptiles": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/@hapi/cryptiles/-/cryptiles-6.0.3.tgz",
+ "integrity": "sha512-r6VKalpbMHz4ci3gFjFysBmhwCg70RpYZy6OkjEpdXzAYnYFX5XsW7n4YMJvuIYpnMwLxGUjK/cBhA7X3JDvXw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@hapi/boom": "^10.0.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@hapi/file": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@hapi/file/-/file-3.0.0.tgz",
+ "integrity": "sha512-w+lKW+yRrLhJu620jT3y+5g2mHqnKfepreykvdOcl9/6up8GrQQn+l3FRTsjHTKbkbfQFkuksHpdv2EcpKcJ4Q==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@hapi/hapi": {
+ "version": "21.4.8",
+ "resolved": "https://registry.npmjs.org/@hapi/hapi/-/hapi-21.4.8.tgz",
+ "integrity": "sha512-l93IrEG4iQyM+yKdngWmkPtajkJGM81yfinmSFmiaNHG+r1fgsWaewwcE1hhsFnqPrVZpU8Y3PiVJMb6uT+01Q==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@hapi/accept": "^6.0.3",
+ "@hapi/ammo": "^6.0.1",
+ "@hapi/boom": "^10.0.1",
+ "@hapi/bounce": "^3.0.2",
+ "@hapi/call": "^9.0.1",
+ "@hapi/catbox": "^12.1.1",
+ "@hapi/catbox-memory": "^6.0.2",
+ "@hapi/heavy": "^8.0.1",
+ "@hapi/hoek": "^11.0.7",
+ "@hapi/mimos": "^7.0.1",
+ "@hapi/podium": "^5.0.2",
+ "@hapi/shot": "^6.0.2",
+ "@hapi/somever": "^4.1.1",
+ "@hapi/statehood": "^8.2.1",
+ "@hapi/subtext": "^8.1.2",
+ "@hapi/teamwork": "^6.0.1",
+ "@hapi/topo": "^6.0.2",
+ "@hapi/validate": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=14.15.0"
+ }
+ },
+ "node_modules/@hapi/heavy": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/@hapi/heavy/-/heavy-8.0.1.tgz",
+ "integrity": "sha512-gBD/NANosNCOp6RsYTsjo2vhr5eYA3BEuogk6cxY0QdhllkkTaJFYtTXv46xd6qhBVMbMMqcSdtqey+UQU3//w==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@hapi/boom": "^10.0.1",
+ "@hapi/hoek": "^11.0.2",
+ "@hapi/validate": "^2.0.1"
+ }
+ },
+ "node_modules/@hapi/hoek": {
+ "version": "11.0.7",
+ "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.7.tgz",
+ "integrity": "sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@hapi/iron": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/@hapi/iron/-/iron-7.0.1.tgz",
+ "integrity": "sha512-tEZnrOujKpS6jLKliyWBl3A9PaE+ppuL/+gkbyPPDb/l2KSKQyH4lhMkVb+sBhwN+qaxxlig01JRqB8dk/mPxQ==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@hapi/b64": "^6.0.1",
+ "@hapi/boom": "^10.0.1",
+ "@hapi/bourne": "^3.0.0",
+ "@hapi/cryptiles": "^6.0.1",
+ "@hapi/hoek": "^11.0.2"
+ }
+ },
+ "node_modules/@hapi/mimos": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/@hapi/mimos/-/mimos-7.0.1.tgz",
+ "integrity": "sha512-b79V+BrG0gJ9zcRx1VGcCI6r6GEzzZUgiGEJVoq5gwzuB2Ig9Cax8dUuBauQCFKvl2YWSWyOc8mZ8HDaJOtkew==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@hapi/hoek": "^11.0.2",
+ "mime-db": "^1.52.0"
+ }
+ },
+ "node_modules/@hapi/nigel": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/@hapi/nigel/-/nigel-5.0.1.tgz",
+ "integrity": "sha512-uv3dtYuB4IsNaha+tigWmN8mQw/O9Qzl5U26Gm4ZcJVtDdB1AVJOwX3X5wOX+A07qzpEZnOMBAm8jjSqGsU6Nw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@hapi/hoek": "^11.0.2",
+ "@hapi/vise": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@hapi/pez": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/@hapi/pez/-/pez-6.1.1.tgz",
+ "integrity": "sha512-yg2OS1tC0S1sHXvhUtWsfRn6lrKl9jKtRhZ+EI0woOW/gqX5vM2PZ1459ypCvCYDRLJ9nIyueeEH5MJV1ZDqIg==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@hapi/b64": "^6.0.1",
+ "@hapi/boom": "^10.0.1",
+ "@hapi/content": "^6.0.1",
+ "@hapi/hoek": "^11.0.7",
+ "@hapi/nigel": "^5.0.1"
+ }
+ },
+ "node_modules/@hapi/podium": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/@hapi/podium/-/podium-5.0.2.tgz",
+ "integrity": "sha512-T7gf2JYHQQfEfewTQFbsaXoZxSvuXO/QBIGljucUQ/lmPnTTNAepoIKOakWNVWvo2fMEDjycu77r8k6dhreqHA==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@hapi/hoek": "^11.0.2",
+ "@hapi/teamwork": "^6.0.0",
+ "@hapi/validate": "^2.0.1"
+ }
+ },
+ "node_modules/@hapi/shot": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/@hapi/shot/-/shot-6.0.2.tgz",
+ "integrity": "sha512-WKK1ShfJTrL1oXC0skoIZQYzvLsyMDEF8lfcWuQBjpjCN29qivr9U36ld1z0nt6edvzv28etNMOqUF4klnHryw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@hapi/hoek": "^11.0.2",
+ "@hapi/validate": "^2.0.1"
+ }
+ },
+ "node_modules/@hapi/somever": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/@hapi/somever/-/somever-4.1.1.tgz",
+ "integrity": "sha512-lt3QQiDDOVRatS0ionFDNrDIv4eXz58IibQaZQDOg4DqqdNme8oa0iPWcE0+hkq/KTeBCPtEOjDOBKBKwDumVg==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@hapi/bounce": "^3.0.1",
+ "@hapi/hoek": "^11.0.2"
+ }
+ },
+ "node_modules/@hapi/statehood": {
+ "version": "8.2.1",
+ "resolved": "https://registry.npmjs.org/@hapi/statehood/-/statehood-8.2.1.tgz",
+ "integrity": "sha512-xf72TG/QINW26jUu+uL5H+crE1o8GplIgfPWwPZhnAGJzetIVAQEQYvzq+C0aEVHg5/lMMtQ+L9UryuSa5Yjkg==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@hapi/boom": "^10.0.1",
+ "@hapi/bounce": "^3.0.1",
+ "@hapi/bourne": "^3.0.0",
+ "@hapi/cryptiles": "^6.0.1",
+ "@hapi/hoek": "^11.0.2",
+ "@hapi/iron": "^7.0.1",
+ "@hapi/validate": "^2.0.1"
+ }
+ },
+ "node_modules/@hapi/subtext": {
+ "version": "8.1.2",
+ "resolved": "https://registry.npmjs.org/@hapi/subtext/-/subtext-8.1.2.tgz",
+ "integrity": "sha512-2x71YJHmFpCjhIhfiNZdKp63nh3xRPp7RrwH7JoO9R4Sd0DRzzRU/VfX2fMmUR7jcoS5qNET1WyGIaqKpMu/ng==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@hapi/boom": "^10.0.1",
+ "@hapi/bourne": "^3.0.0",
+ "@hapi/content": "^6.0.1",
+ "@hapi/file": "^3.0.0",
+ "@hapi/hoek": "^11.0.7",
+ "@hapi/pez": "^6.1.1",
+ "@hapi/wreck": "^18.1.0"
+ }
+ },
+ "node_modules/@hapi/teamwork": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/@hapi/teamwork/-/teamwork-6.0.1.tgz",
+ "integrity": "sha512-52OXRslUfYwXAOG8k58f2h2ngXYQGP0x5RPOo+eWA/FtyLgHjGMrE3+e9LSXP/0q2YfHAK5wj9aA9DTy1K+kyQ==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@hapi/topo": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.2.tgz",
+ "integrity": "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@hapi/hoek": "^11.0.2"
+ }
+ },
+ "node_modules/@hapi/validate": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@hapi/validate/-/validate-2.0.1.tgz",
+ "integrity": "sha512-NZmXRnrSLK8MQ9y/CMqE9WSspgB9xA41/LlYR0k967aSZebWr4yNrpxIbov12ICwKy4APSlWXZga9jN5p6puPA==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@hapi/hoek": "^11.0.2",
+ "@hapi/topo": "^6.0.1"
+ }
+ },
+ "node_modules/@hapi/vise": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/@hapi/vise/-/vise-5.0.1.tgz",
+ "integrity": "sha512-XZYWzzRtINQLedPYlIkSkUr7m5Ddwlu99V9elh8CSygXstfv3UnWIXT0QD+wmR0VAG34d2Vx3olqcEhRRoTu9A==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@hapi/hoek": "^11.0.2"
+ }
+ },
+ "node_modules/@hapi/wreck": {
+ "version": "18.1.2",
+ "resolved": "https://registry.npmjs.org/@hapi/wreck/-/wreck-18.1.2.tgz",
+ "integrity": "sha512-3dMnV2pfhQiyEqu8DL3VBmxkdLiRDiiUDuG79Dp+UK1gL9ZxAfDOUhB6k3D5MLqcgJJ1IARyGFhwoc1NITr/pg==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@hapi/boom": "^10.0.1",
+ "@hapi/bourne": "^3.0.0",
+ "@hapi/hoek": "^11.0.2"
+ }
+ },
"node_modules/@humanfs/core": {
"version": "0.19.1",
"dev": true,
@@ -4185,104 +4493,464 @@
"dev": true,
"license": "MIT"
},
- "node_modules/@types/activedirectory2": {
- "version": "1.2.6",
- "resolved": "https://registry.npmjs.org/@types/activedirectory2/-/activedirectory2-1.2.6.tgz",
- "integrity": "sha512-mJsoOWf9LRpYBkExOWstWe6g6TQnZyZjVULNrX8otcCJgVliesk9T/+W+1ahrx2zaevxsp28sSKOwo/b7TOnSg==",
- "dev": true,
+ "node_modules/@tsoa/cli": {
+ "version": "7.0.0-alpha.0",
+ "resolved": "https://registry.npmjs.org/@tsoa/cli/-/cli-7.0.0-alpha.0.tgz",
+ "integrity": "sha512-fCBWv6F20qrpwFh5X+vaZs38Bh/5cVwKPhvG/uArR+crZgEowyNl76unLCmYdvycES9Y6g/TKFLagG8qojSNuQ==",
"license": "MIT",
"dependencies": {
- "@types/ldapjs": "*"
+ "@tsoa/runtime": "^7.0.0-alpha.0",
+ "@types/multer": "^1.4.12",
+ "fs-extra": "^11.2.0",
+ "glob": "^10.3.10",
+ "handlebars": "^4.7.8",
+ "merge-anything": "^5.1.7",
+ "minimatch": "^9.0.1",
+ "ts-deepmerge": "^7.0.2",
+ "typescript": "^5.7.2",
+ "validator": "^13.12.0",
+ "yaml": "^2.6.1",
+ "yargs": "^17.7.1"
+ },
+ "bin": {
+ "tsoa": "dist/cli.js"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "yarn": ">=1.9.4"
}
},
- "node_modules/@types/babel__core": {
- "version": "7.20.5",
- "dev": true,
+ "node_modules/@tsoa/cli/node_modules/brace-expansion": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz",
+ "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==",
"license": "MIT",
"dependencies": {
- "@babel/parser": "^7.20.7",
- "@babel/types": "^7.20.7",
- "@types/babel__generator": "*",
- "@types/babel__template": "*",
- "@types/babel__traverse": "*"
+ "balanced-match": "^1.0.0"
}
},
- "node_modules/@types/babel__generator": {
- "version": "7.6.8",
- "dev": true,
+ "node_modules/@tsoa/cli/node_modules/fs-extra": {
+ "version": "11.3.4",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz",
+ "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==",
"license": "MIT",
"dependencies": {
- "@babel/types": "^7.0.0"
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=14.14"
}
},
- "node_modules/@types/babel__template": {
- "version": "7.4.4",
- "dev": true,
- "license": "MIT",
+ "node_modules/@tsoa/cli/node_modules/minimatch": {
+ "version": "9.0.9",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
+ "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
+ "license": "ISC",
"dependencies": {
- "@babel/parser": "^7.1.0",
- "@babel/types": "^7.0.0"
+ "brace-expansion": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
}
},
- "node_modules/@types/babel__traverse": {
- "version": "7.20.5",
- "dev": true,
+ "node_modules/@tsoa/runtime": {
+ "version": "7.0.0-alpha.0",
+ "resolved": "https://registry.npmjs.org/@tsoa/runtime/-/runtime-7.0.0-alpha.0.tgz",
+ "integrity": "sha512-zlWYz2bLfaN6WtFoIbLBEAyVhKG4IQKJ9QPzeRFFKeAsh5zYN4/ocnd14XyWs4ehY9TdtTnma2drW89aYNSRYw==",
"license": "MIT",
"dependencies": {
- "@babel/types": "^7.20.7"
+ "@hapi/boom": "^10.0.1",
+ "@hapi/hapi": "^21.3.12",
+ "@types/koa": "^2.15.0",
+ "@types/multer": "^1.4.12",
+ "express": "^4.21.2",
+ "reflect-metadata": "^0.2.2",
+ "validator": "^13.12.0"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "yarn": ">=1.9.4"
}
},
- "node_modules/@types/body-parser": {
- "version": "1.19.5",
- "dev": true,
+ "node_modules/@tsoa/runtime/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==",
"license": "MIT",
"dependencies": {
- "@types/connect": "*",
- "@types/node": "*"
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
}
},
- "node_modules/@types/connect": {
- "version": "3.4.38",
- "dev": true,
+ "node_modules/@tsoa/runtime/node_modules/body-parser": {
+ "version": "1.20.5",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz",
+ "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==",
"license": "MIT",
"dependencies": {
- "@types/node": "*"
+ "bytes": "~3.1.2",
+ "content-type": "~1.0.5",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "~1.2.0",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "on-finished": "~2.4.1",
+ "qs": "~6.15.1",
+ "raw-body": "~2.5.3",
+ "type-is": "~1.6.18",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
}
},
- "node_modules/@types/conventional-commits-parser": {
- "version": "5.0.1",
- "dev": true,
+ "node_modules/@tsoa/runtime/node_modules/content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
"license": "MIT",
"dependencies": {
- "@types/node": "*"
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
}
},
- "node_modules/@types/cookiejar": {
- "version": "2.1.5",
- "dev": true,
+ "node_modules/@tsoa/runtime/node_modules/cookie-signature": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
+ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
"license": "MIT"
},
- "node_modules/@types/cors": {
- "version": "2.8.19",
- "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
- "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
- "dev": true,
+ "node_modules/@tsoa/runtime/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"license": "MIT",
"dependencies": {
- "@types/node": "*"
+ "ms": "2.0.0"
}
},
- "node_modules/@types/deep-eql": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
- "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
- "dev": true,
+ "node_modules/@tsoa/runtime/node_modules/debug/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
- "node_modules/@types/domhandler": {
- "version": "2.4.5",
- "dev": true,
- "license": "MIT"
+ "node_modules/@tsoa/runtime/node_modules/express": {
+ "version": "4.22.2",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz",
+ "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "~1.20.5",
+ "content-disposition": "~0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "~0.7.1",
+ "cookie-signature": "~1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "~1.3.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.0",
+ "merge-descriptors": "1.0.3",
+ "methods": "~1.1.2",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "~0.1.12",
+ "proxy-addr": "~2.0.7",
+ "qs": "~6.15.1",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "~0.19.0",
+ "serve-static": "~1.16.2",
+ "setprototypeof": "1.2.0",
+ "statuses": "~2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/@tsoa/runtime/node_modules/finalhandler": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
+ "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "~2.0.2",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/@tsoa/runtime/node_modules/fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/@tsoa/runtime/node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/@tsoa/runtime/node_modules/merge-descriptors": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@tsoa/runtime/node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@tsoa/runtime/node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/@tsoa/runtime/node_modules/path-to-regexp": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
+ "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
+ "license": "MIT"
+ },
+ "node_modules/@tsoa/runtime/node_modules/send": {
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
+ "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.1",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "~2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "~2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/@tsoa/runtime/node_modules/serve-static": {
+ "version": "1.16.3",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
+ "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "~0.19.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/@tsoa/runtime/node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "license": "MIT",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/@types/accepts": {
+ "version": "1.3.7",
+ "resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.7.tgz",
+ "integrity": "sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/activedirectory2": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@types/activedirectory2/-/activedirectory2-1.2.6.tgz",
+ "integrity": "sha512-mJsoOWf9LRpYBkExOWstWe6g6TQnZyZjVULNrX8otcCJgVliesk9T/+W+1ahrx2zaevxsp28sSKOwo/b7TOnSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/ldapjs": "*"
+ }
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.6.8",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.20.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.20.7"
+ }
+ },
+ "node_modules/@types/body-parser": {
+ "version": "1.19.5",
+ "license": "MIT",
+ "dependencies": {
+ "@types/connect": "*",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/chai": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
+ "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/deep-eql": "*",
+ "assertion-error": "^2.0.1"
+ }
+ },
+ "node_modules/@types/connect": {
+ "version": "3.4.38",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/content-disposition": {
+ "version": "0.5.9",
+ "resolved": "https://registry.npmjs.org/@types/content-disposition/-/content-disposition-0.5.9.tgz",
+ "integrity": "sha512-8uYXI3Gw35MhiVYhG3s295oihrxRyytcRHjSjqnqZVDDy/xcGBRny7+Xj1Wgfhv5QzRtN2hB2dVRBUX9XW3UcQ==",
+ "license": "MIT"
+ },
+ "node_modules/@types/conventional-commits-parser": {
+ "version": "5.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/cookiejar": {
+ "version": "2.1.5",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/cookies": {
+ "version": "0.9.2",
+ "resolved": "https://registry.npmjs.org/@types/cookies/-/cookies-0.9.2.tgz",
+ "integrity": "sha512-1AvkDdZM2dbyFybL4fxpuNCaWyv//0AwsuUk2DWeXyM1/5ZKm6W3z6mQi24RZ4l2ucY+bkSHzbDVpySqPGuV8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/connect": "*",
+ "@types/express": "*",
+ "@types/keygrip": "*",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/cors": {
+ "version": "2.8.19",
+ "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
+ "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/deep-eql": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
+ "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/domhandler": {
+ "version": "2.4.5",
+ "dev": true,
+ "license": "MIT"
},
"node_modules/@types/domutils": {
"version": "2.1.0",
@@ -4304,7 +4972,6 @@
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz",
"integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@types/body-parser": "*",
@@ -4324,7 +4991,6 @@
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz",
"integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*",
@@ -4354,9 +5020,14 @@
"domhandler": "^2.4.0"
}
},
+ "node_modules/@types/http-assert": {
+ "version": "1.5.6",
+ "resolved": "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.5.6.tgz",
+ "integrity": "sha512-TTEwmtjgVbYAzZYWyeHPrrtWnfVkm8tQkP8P21uQifPgMRgjrow3XDEYqucuC8SKZJT7pUnhU/JymvjggxO9vw==",
+ "license": "MIT"
+ },
"node_modules/@types/http-errors": {
"version": "2.0.4",
- "dev": true,
"license": "MIT"
},
"node_modules/@types/istanbul-lib-coverage": {
@@ -4382,6 +5053,37 @@
"@types/node": "*"
}
},
+ "node_modules/@types/keygrip": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.6.tgz",
+ "integrity": "sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ==",
+ "license": "MIT"
+ },
+ "node_modules/@types/koa": {
+ "version": "2.15.0",
+ "resolved": "https://registry.npmjs.org/@types/koa/-/koa-2.15.0.tgz",
+ "integrity": "sha512-7QFsywoE5URbuVnG3loe03QXuGajrnotr3gQkXcEBShORai23MePfFYdhz90FEtBBpkyIYQbVD+evKtloCgX3g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/accepts": "*",
+ "@types/content-disposition": "*",
+ "@types/cookies": "*",
+ "@types/http-assert": "*",
+ "@types/http-errors": "*",
+ "@types/keygrip": "*",
+ "@types/koa-compose": "*",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/koa-compose": {
+ "version": "3.2.9",
+ "resolved": "https://registry.npmjs.org/@types/koa-compose/-/koa-compose-3.2.9.tgz",
+ "integrity": "sha512-BroAZ9FTvPiCy0Pi8tjD1OfJ7bgU1gQf0eR6e1Vm+JJATy9eKOG3hQMFtMciMawiSOVnLMdmUOC46s7HBhSTsA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/koa": "*"
+ }
+ },
"node_modules/@types/ldapjs": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/@types/ldapjs/-/ldapjs-3.0.6.tgz",
@@ -4418,7 +5120,6 @@
},
"node_modules/@types/mime": {
"version": "1.3.5",
- "dev": true,
"license": "MIT"
},
"node_modules/@types/ms": {
@@ -4428,6 +5129,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/multer": {
+ "version": "1.4.13",
+ "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.13.tgz",
+ "integrity": "sha512-bhhdtPw7JqCiEfC9Jimx5LqX9BDIPJEh2q/fQ4bqbBPtyEZYr3cvF22NwG0DmPZNYA0CAf2CnqDB4KIGGpJcaw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/express": "*"
+ }
+ },
"node_modules/@types/node": {
"version": "22.19.8",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.8.tgz",
@@ -4476,12 +5186,10 @@
},
"node_modules/@types/qs": {
"version": "6.9.18",
- "dev": true,
"license": "MIT"
},
"node_modules/@types/range-parser": {
"version": "1.2.7",
- "dev": true,
"license": "MIT"
},
"node_modules/@types/react": {
@@ -4527,7 +5235,6 @@
},
"node_modules/@types/send": {
"version": "0.17.4",
- "dev": true,
"license": "MIT",
"dependencies": {
"@types/mime": "^1",
@@ -4538,7 +5245,6 @@
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz",
"integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@types/http-errors": "*",
@@ -4645,15 +5351,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/@types/yauzl": {
- "version": "2.10.3",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "@types/node": "*"
- }
- },
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.56.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.0.tgz",
@@ -4958,9 +5655,9 @@
}
},
"node_modules/@vitest/coverage-v8": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz",
- "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==",
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.6.tgz",
+ "integrity": "sha512-LsAdmUapA0qSN306d8+zOyawM0hFm2m2Hg9IwVNIKBm+qJV8cijiq2c+gxKZcB1HCfIWAy+0qEZDCUQA58A1cw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4982,8 +5679,8 @@
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
- "@vitest/browser": "3.2.4",
- "vitest": "3.2.4"
+ "@vitest/browser": "3.2.6",
+ "vitest": "3.2.6"
},
"peerDependenciesMeta": {
"@vitest/browser": {
@@ -5048,15 +5745,15 @@
}
},
"node_modules/@vitest/expect": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz",
- "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==",
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.6.tgz",
+ "integrity": "sha512-1+7q9BtaKzEmO+fmNT3kYvoNn5Y71XWAx2Q5HRim4tTVRQVRv4uJFAQ5FbK0OPUeNP/WmVCpxYxoJdvuHVjzBQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/chai": "^5.2.2",
- "@vitest/spy": "3.2.4",
- "@vitest/utils": "3.2.4",
+ "@vitest/spy": "3.2.6",
+ "@vitest/utils": "3.2.6",
"chai": "^5.2.0",
"tinyrainbow": "^2.0.0"
},
@@ -5064,84 +5761,37 @@
"url": "https://opencollective.com/vitest"
}
},
- "node_modules/@vitest/expect/node_modules/@types/chai": {
- "version": "5.2.2",
- "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz",
- "integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==",
+ "node_modules/@vitest/mocker": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.6.tgz",
+ "integrity": "sha512-EZOrpDbkKotFAP7wPAQV1UIyoGOk4oX7ynWhBhLB7v+meMHbQhU16oPpIYGTTe4oFlhpryGpgpcZP/sin3hYuw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@types/deep-eql": "*"
+ "@vitest/spy": "3.2.6",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.17"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "msw": "^2.4.9",
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
}
},
- "node_modules/@vitest/expect/node_modules/assertion-error": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
- "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@vitest/expect/node_modules/chai": {
- "version": "5.3.3",
- "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
- "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "assertion-error": "^2.0.1",
- "check-error": "^2.1.1",
- "deep-eql": "^5.0.1",
- "loupe": "^3.1.0",
- "pathval": "^2.0.0"
- },
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@vitest/expect/node_modules/check-error": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz",
- "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 16"
- }
- },
- "node_modules/@vitest/expect/node_modules/deep-eql": {
- "version": "5.0.2",
- "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
- "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/@vitest/expect/node_modules/loupe": {
- "version": "3.2.1",
- "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
- "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@vitest/expect/node_modules/pathval": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz",
- "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 14.16"
- }
- },
- "node_modules/@vitest/pretty-format": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz",
- "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==",
+ "node_modules/@vitest/pretty-format": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.6.tgz",
+ "integrity": "sha512-lb7XXXzmm2h2ASzFnRvQpDo6onT1NmMJA3tkGTWiBFtRJ9lxGY3d3mm/Apt36gej2bkkOVLL/yTOtufDaFa/jA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5152,13 +5802,13 @@
}
},
"node_modules/@vitest/runner": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz",
- "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==",
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.6.tgz",
+ "integrity": "sha512-HYcoSj1w5tcgUnzoF0HcyaAQjpA1gj9ftUJ7iSJSuipc02jW9gKkigwZbjFldAfYHA1fa8UZVRftdMY5msWM9Q==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/utils": "3.2.4",
+ "@vitest/utils": "3.2.6",
"pathe": "^2.0.3",
"strip-literal": "^3.0.0"
},
@@ -5167,13 +5817,13 @@
}
},
"node_modules/@vitest/snapshot": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz",
- "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==",
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.6.tgz",
+ "integrity": "sha512-H+ZjNTWGpObenh0YnlBctAPnJSI20P81PL8BPzWpx54YXLLTm8hEsWawtcYLMrwvpK48hGxLLbCS+1KRXhsKhw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/pretty-format": "3.2.4",
+ "@vitest/pretty-format": "3.2.6",
"magic-string": "^0.30.17",
"pathe": "^2.0.3"
},
@@ -5182,9 +5832,9 @@
}
},
"node_modules/@vitest/spy": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz",
- "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==",
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.6.tgz",
+ "integrity": "sha512-oq6BbH68WzcWmwtBrU9nqLeaXTR4XwJF7FSLkKEZo4i6eoXcrxjcwSuTvWBIRUTC6VC72nXYunzqgZA+IKdtxg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5195,13 +5845,13 @@
}
},
"node_modules/@vitest/utils": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz",
- "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==",
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.6.tgz",
+ "integrity": "sha512-lI23nIs4bnT3T8NIoh+vFaz5s2/DdP0Jgt2jxwgWljvwn82cLJtyi/If+fjFyoLMGIOz0U/fKvWE0d4jsNQEfg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/pretty-format": "3.2.4",
+ "@vitest/pretty-format": "3.2.6",
"loupe": "^3.1.4",
"tinyrainbow": "^2.0.0"
},
@@ -5209,13 +5859,6 @@
"url": "https://opencollective.com/vitest"
}
},
- "node_modules/@vitest/utils/node_modules/loupe": {
- "version": "3.2.1",
- "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
- "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/abbrev": {
"version": "1.1.1",
"license": "ISC"
@@ -5353,34 +5996,17 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
- "node_modules/ansi-colors": {
- "version": "4.1.3",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
"node_modules/ansi-escapes": {
- "version": "4.3.2",
+ "version": "7.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz",
+ "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "type-fest": "^0.21.3"
- },
- "engines": {
- "node": ">=8"
+ "environment": "^1.0.0"
},
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/ansi-escapes/node_modules/type-fest": {
- "version": "0.21.3",
- "dev": true,
- "license": "(MIT OR CC0-1.0)",
"engines": {
- "node": ">=10"
+ "node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
@@ -5474,6 +6100,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
+ "license": "MIT"
+ },
"node_modules/array-ify": {
"version": "1.0.0",
"dev": true,
@@ -5617,6 +6249,16 @@
"node": ">=0.8"
}
},
+ "node_modules/assertion-error": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/ast-v8-to-istanbul": {
"version": "0.3.7",
"resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.7.tgz",
@@ -5636,14 +6278,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/astral-regex": {
- "version": "2.0.0",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/async": {
"version": "3.2.5",
"license": "MIT"
@@ -5687,6 +6321,8 @@
},
"node_modules/aws-sign2": {
"version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
+ "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
@@ -5695,6 +6331,8 @@
},
"node_modules/aws4": {
"version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz",
+ "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==",
"dev": true,
"license": "MIT"
},
@@ -5823,26 +6461,6 @@
"url": "https://opencollective.com/express"
}
},
- "node_modules/body-parser/node_modules/http-errors": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
- "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
- "license": "MIT",
- "dependencies": {
- "depd": "~2.0.0",
- "inherits": "~2.0.4",
- "setprototypeof": "~1.2.0",
- "statuses": "~2.0.2",
- "toidentifier": "~1.0.1"
- },
- "engines": {
- "node": ">= 0.8"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
"node_modules/body-parser/node_modules/iconv-lite": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz",
@@ -5874,15 +6492,6 @@
"node": ">= 0.10"
}
},
- "node_modules/body-parser/node_modules/statuses": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
- "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
"node_modules/bowser": {
"version": "2.13.1",
"resolved": "https://registry.npmjs.org/bowser/-/bowser-2.13.1.tgz",
@@ -5966,14 +6575,6 @@
"ieee754": "^1.1.13"
}
},
- "node_modules/buffer-crc32": {
- "version": "0.2.13",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "*"
- }
- },
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
@@ -6041,9 +6642,9 @@
}
},
"node_modules/c8/node_modules/brace-expansion": {
- "version": "5.0.5",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
- "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
+ "version": "5.0.6",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
+ "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -6239,9 +6840,28 @@
},
"node_modules/caseless": {
"version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
+ "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==",
"dev": true,
"license": "Apache-2.0"
},
+ "node_modules/chai": {
+ "version": "5.3.3",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
+ "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "assertion-error": "^2.0.1",
+ "check-error": "^2.1.1",
+ "deep-eql": "^5.0.1",
+ "loupe": "^3.1.0",
+ "pathval": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/chalk": {
"version": "4.1.2",
"license": "MIT",
@@ -6280,6 +6900,16 @@
"node": ">=8"
}
},
+ "node_modules/check-error": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz",
+ "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 16"
+ }
+ },
"node_modules/ci-info": {
"version": "4.3.0",
"funding": [
@@ -6306,14 +6936,19 @@
}
},
"node_modules/cli-cursor": {
- "version": "3.1.0",
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
+ "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "restore-cursor": "^3.1.0"
+ "restore-cursor": "^5.0.0"
},
"engines": {
- "node": ">=8"
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cli-table3": {
@@ -6349,36 +6984,66 @@
}
},
"node_modules/cli-truncate": {
- "version": "2.1.0",
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.2.0.tgz",
+ "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "slice-ansi": "^3.0.0",
- "string-width": "^4.2.0"
+ "slice-ansi": "^8.0.0",
+ "string-width": "^8.2.0"
},
"engines": {
- "node": ">=8"
+ "node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/cli-truncate/node_modules/emoji-regex": {
- "version": "8.0.0",
+ "node_modules/cli-truncate/node_modules/ansi-regex": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
+ "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
"dev": true,
- "license": "MIT"
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
},
"node_modules/cli-truncate/node_modules/string-width": {
- "version": "4.2.3",
+ "version": "8.2.1",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz",
+ "integrity": "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "emoji-regex": "^8.0.0",
- "is-fullwidth-code-point": "^3.0.0",
- "strip-ansi": "^6.0.1"
+ "get-east-asian-width": "^1.5.0",
+ "strip-ansi": "^7.1.2"
},
"engines": {
- "node": ">=8"
+ "node": ">=20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cli-truncate/node_modules/strip-ansi": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
+ "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.2.2"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/cliui": {
@@ -6835,14 +7500,14 @@
"license": "MIT"
},
"node_modules/cypress": {
- "version": "15.9.0",
- "resolved": "https://registry.npmjs.org/cypress/-/cypress-15.9.0.tgz",
- "integrity": "sha512-Ks6Bdilz3TtkLZtTQyqYaqtL/WT3X3APKaSLhTV96TmTyudzSjc6EJsJCHmBb7DxO+3R12q3Jkbjgm/iPgmwfg==",
+ "version": "15.16.0",
+ "resolved": "https://registry.npmjs.org/cypress/-/cypress-15.16.0.tgz",
+ "integrity": "sha512-fy0M0c9xDLEp4v9y7LLKFeAQhIdDsobxDSKpD3JcZpqQefjy9TSzEyVV3HA0zu7hUi0bGHlSYlI7ASub8wgR9A==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
- "@cypress/request": "^3.0.10",
+ "@cypress/request": "^4.0.0",
"@cypress/xvfb": "^1.2.4",
"@types/sinonjs__fake-timers": "8.1.1",
"@types/sizzle": "^2.3.2",
@@ -6851,26 +7516,22 @@
"blob-util": "^2.0.2",
"bluebird": "^3.7.2",
"buffer": "^5.7.1",
- "cachedir": "^2.3.0",
+ "cachedir": "^2.4.0",
"chalk": "^4.1.0",
"ci-info": "^4.1.0",
- "cli-cursor": "^3.1.0",
"cli-table3": "0.6.1",
"commander": "^6.2.1",
"common-tags": "^1.8.0",
"dayjs": "^1.10.4",
"debug": "^4.3.4",
- "enquirer": "^2.3.6",
"eventemitter2": "6.4.7",
"execa": "4.1.0",
"executable": "^4.1.1",
- "extract-zip": "2.0.1",
- "figures": "^3.2.0",
"fs-extra": "^9.1.0",
"hasha": "5.2.2",
"is-installed-globally": "~0.4.0",
- "listr2": "^3.8.3",
- "lodash": "^4.17.21",
+ "listr2": "^9.0.5",
+ "lodash": "^4.17.23",
"log-symbols": "^4.0.0",
"minimist": "^1.2.8",
"ospath": "^1.2.2",
@@ -6879,11 +7540,12 @@
"proxy-from-env": "1.0.0",
"request-progress": "^3.0.0",
"supports-color": "^8.1.1",
- "systeminformation": "^5.27.14",
+ "systeminformation": "^5.31.1",
"tmp": "~0.2.4",
"tree-kill": "1.2.2",
+ "tslib": "1.14.1",
"untildify": "^4.0.0",
- "yauzl": "^2.10.0"
+ "yauzl": "^3.3.1"
},
"bin": {
"cypress": "bin/cypress"
@@ -6899,6 +7561,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/cypress/node_modules/tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
+ "dev": true,
+ "license": "0BSD"
+ },
"node_modules/dargs": {
"version": "8.1.0",
"dev": true,
@@ -6912,6 +7581,8 @@
},
"node_modules/dashdash": {
"version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
+ "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -7012,6 +7683,16 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/deep-eql": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
+ "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/deep-is": {
"version": "0.1.4",
"dev": true,
@@ -7084,6 +7765,16 @@
"node": ">= 0.8"
}
},
+ "node_modules/destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
"node_modules/dezalgo": {
"version": "1.0.4",
"dev": true,
@@ -7220,6 +7911,8 @@
},
"node_modules/ecc-jsbn": {
"version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
+ "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -7266,18 +7959,6 @@
"once": "^1.4.0"
}
},
- "node_modules/enquirer": {
- "version": "2.4.1",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-colors": "^4.1.1",
- "strip-ansi": "^6.0.1"
- },
- "engines": {
- "node": ">=8.6"
- }
- },
"node_modules/entities": {
"version": "1.1.2",
"license": "BSD-2-Clause"
@@ -8066,28 +8747,11 @@
},
"node_modules/extend": {
"version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
"dev": true,
"license": "MIT"
},
- "node_modules/extract-zip": {
- "version": "2.0.1",
- "dev": true,
- "license": "BSD-2-Clause",
- "dependencies": {
- "debug": "^4.1.1",
- "get-stream": "^5.1.0",
- "yauzl": "^2.10.0"
- },
- "bin": {
- "extract-zip": "cli.js"
- },
- "engines": {
- "node": ">= 10.17.0"
- },
- "optionalDependencies": {
- "@types/yauzl": "^2.9.1"
- }
- },
"node_modules/extsprintf": {
"version": "1.4.1",
"engines": [
@@ -8192,38 +8856,6 @@
"fxparser": "src/cli/cli.js"
}
},
- "node_modules/fd-slicer": {
- "version": "1.1.0",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "pend": "~1.2.0"
- }
- },
- "node_modules/figures": {
- "version": "3.2.0",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "escape-string-regexp": "^1.0.5"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/figures/node_modules/escape-string-regexp": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
- "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.8.0"
- }
- },
"node_modules/file-entry-cache": {
"version": "8.0.0",
"dev": true,
@@ -8385,6 +9017,8 @@
},
"node_modules/forever-agent": {
"version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
+ "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==",
"dev": true,
"license": "Apache-2.0",
"engines": {
@@ -8637,6 +9271,8 @@
},
"node_modules/getpass": {
"version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
+ "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -8795,7 +9431,6 @@
},
"node_modules/graceful-fs": {
"version": "4.2.11",
- "dev": true,
"license": "ISC"
},
"node_modules/graphql": {
@@ -8806,6 +9441,27 @@
"iterall": "1.1.3"
}
},
+ "node_modules/handlebars": {
+ "version": "4.7.9",
+ "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz",
+ "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==",
+ "license": "MIT",
+ "dependencies": {
+ "minimist": "^1.2.5",
+ "neo-async": "^2.6.2",
+ "source-map": "^0.6.1",
+ "wordwrap": "^1.0.0"
+ },
+ "bin": {
+ "handlebars": "bin/handlebars"
+ },
+ "engines": {
+ "node": ">=0.4.7"
+ },
+ "optionalDependencies": {
+ "uglify-js": "^3.1.4"
+ }
+ },
"node_modules/has-bigints": {
"version": "1.1.0",
"dev": true,
@@ -8950,21 +9606,29 @@
}
},
"node_modules/http-errors": {
- "version": "2.0.0",
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
"license": "MIT",
"dependencies": {
- "depd": "2.0.0",
- "inherits": "2.0.4",
- "setprototypeof": "1.2.0",
- "statuses": "2.0.1",
- "toidentifier": "1.0.1"
+ "depd": "~2.0.0",
+ "inherits": "~2.0.4",
+ "setprototypeof": "~1.2.0",
+ "statuses": "~2.0.2",
+ "toidentifier": "~1.0.1"
},
"engines": {
"node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
}
},
"node_modules/http-signature": {
"version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.4.0.tgz",
+ "integrity": "sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -9599,6 +10263,18 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-what": {
+ "version": "4.1.16",
+ "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz",
+ "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.13"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mesqueeb"
+ }
+ },
"node_modules/is-windows": {
"version": "1.0.2",
"dev": true,
@@ -9691,6 +10367,8 @@
},
"node_modules/isstream": {
"version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
+ "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==",
"dev": true,
"license": "MIT"
},
@@ -9935,6 +10613,8 @@
},
"node_modules/jsbn": {
"version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
+ "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==",
"dev": true,
"license": "MIT"
},
@@ -9961,6 +10641,8 @@
},
"node_modules/json-schema": {
"version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
+ "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==",
"dev": true,
"license": "(AFL-2.1 OR BSD-3-Clause)"
},
@@ -9976,6 +10658,8 @@
},
"node_modules/json-stringify-safe": {
"version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
+ "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
"dev": true,
"license": "ISC"
},
@@ -9992,7 +10676,6 @@
},
"node_modules/jsonfile": {
"version": "6.1.0",
- "dev": true,
"license": "MIT",
"dependencies": {
"universalify": "^2.0.0"
@@ -10060,6 +10743,8 @@
},
"node_modules/jsprim": {
"version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz",
+ "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==",
"dev": true,
"engines": [
"node >=0.6.0"
@@ -10074,6 +10759,8 @@
},
"node_modules/jsprim/node_modules/extsprintf": {
"version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
+ "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==",
"dev": true,
"engines": [
"node >=0.6.0"
@@ -10082,6 +10769,8 @@
},
"node_modules/jsprim/node_modules/verror": {
"version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
+ "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==",
"dev": true,
"engines": [
"node >=0.6.0"
@@ -10300,22 +10989,6 @@
"yaml": "^2.8.4"
}
},
- "node_modules/lint-staged/node_modules/ansi-escapes": {
- "version": "7.3.0",
- "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz",
- "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "environment": "^1.0.0"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/lint-staged/node_modules/ansi-regex": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
@@ -10342,62 +11015,6 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
- "node_modules/lint-staged/node_modules/cli-cursor": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
- "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "restore-cursor": "^5.0.0"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/lint-staged/node_modules/cli-truncate": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.2.0.tgz",
- "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "slice-ansi": "^8.0.0",
- "string-width": "^8.2.0"
- },
- "engines": {
- "node": ">=20"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/lint-staged/node_modules/emoji-regex": {
- "version": "10.6.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
- "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/lint-staged/node_modules/is-fullwidth-code-point": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz",
- "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "get-east-asian-width": "^1.3.1"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/lint-staged/node_modules/listr2": {
"version": "10.2.1",
"resolved": "https://registry.npmjs.org/listr2/-/listr2-10.2.1.tgz",
@@ -10415,31 +11032,11 @@
"node": ">=22.13.0"
}
},
- "node_modules/lint-staged/node_modules/log-update": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz",
- "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-escapes": "^7.0.0",
- "cli-cursor": "^5.0.0",
- "slice-ansi": "^7.1.0",
- "strip-ansi": "^7.1.0",
- "wrap-ansi": "^9.0.0"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/lint-staged/node_modules/log-update/node_modules/slice-ansi": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz",
"integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==",
- "dev": true,
+ "extraneous": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.2.1",
@@ -10456,7 +11053,7 @@
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
- "dev": true,
+ "extraneous": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^10.3.0",
@@ -10474,7 +11071,7 @@
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
"integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
- "dev": true,
+ "extraneous": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.2.1",
@@ -10488,69 +11085,6 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
- "node_modules/lint-staged/node_modules/onetime": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz",
- "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "mimic-function": "^5.0.0"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/lint-staged/node_modules/restore-cursor": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz",
- "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "onetime": "^7.0.0",
- "signal-exit": "^4.1.0"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/lint-staged/node_modules/signal-exit": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
- "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
- "dev": true,
- "license": "ISC",
- "engines": {
- "node": ">=14"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/lint-staged/node_modules/slice-ansi": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz",
- "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^6.2.3",
- "is-fullwidth-code-point": "^5.1.0"
- },
- "engines": {
- "node": ">=20"
- },
- "funding": {
- "url": "https://github.com/chalk/slice-ansi?sponsor=1"
- }
- },
"node_modules/lint-staged/node_modules/string-width": {
"version": "8.2.1",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz",
@@ -10581,7 +11115,7 @@
"node": ">=12"
},
"funding": {
- "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/lint-staged/node_modules/wrap-ansi": {
@@ -10599,78 +11133,107 @@
"node": ">=20"
},
"funding": {
- "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/listr2": {
- "version": "3.14.0",
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz",
+ "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==",
"dev": true,
"license": "MIT",
"dependencies": {
- "cli-truncate": "^2.1.0",
- "colorette": "^2.0.16",
- "log-update": "^4.0.0",
- "p-map": "^4.0.0",
- "rfdc": "^1.3.0",
- "rxjs": "^7.5.1",
- "through": "^2.3.8",
- "wrap-ansi": "^7.0.0"
+ "cli-truncate": "^5.0.0",
+ "colorette": "^2.0.20",
+ "eventemitter3": "^5.0.1",
+ "log-update": "^6.1.0",
+ "rfdc": "^1.4.1",
+ "wrap-ansi": "^9.0.0"
},
"engines": {
- "node": ">=10.0.0"
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/listr2/node_modules/ansi-regex": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
+ "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
},
- "peerDependencies": {
- "enquirer": ">= 2.3.0 < 3"
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/listr2/node_modules/ansi-styles": {
+ "version": "6.2.3",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
+ "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
},
- "peerDependenciesMeta": {
- "enquirer": {
- "optional": true
- }
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/listr2/node_modules/emoji-regex": {
- "version": "8.0.0",
+ "version": "10.6.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
+ "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
"dev": true,
"license": "MIT"
},
- "node_modules/listr2/node_modules/p-map": {
- "version": "4.0.0",
+ "node_modules/listr2/node_modules/string-width": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
+ "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "aggregate-error": "^3.0.0"
+ "emoji-regex": "^10.3.0",
+ "get-east-asian-width": "^1.0.0",
+ "strip-ansi": "^7.1.0"
},
"engines": {
- "node": ">=10"
+ "node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/listr2/node_modules/string-width": {
- "version": "4.2.3",
+ "node_modules/listr2/node_modules/strip-ansi": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
+ "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
"dev": true,
"license": "MIT",
"dependencies": {
- "emoji-regex": "^8.0.0",
- "is-fullwidth-code-point": "^3.0.0",
- "strip-ansi": "^6.0.1"
+ "ansi-regex": "^6.2.2"
},
"engines": {
- "node": ">=8"
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/listr2/node_modules/wrap-ansi": {
- "version": "7.0.0",
+ "version": "9.0.2",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
+ "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
"dev": true,
"license": "MIT",
"dependencies": {
- "ansi-styles": "^4.0.0",
- "string-width": "^4.1.0",
- "strip-ansi": "^6.0.0"
+ "ansi-styles": "^6.2.1",
+ "string-width": "^7.0.0",
+ "strip-ansi": "^7.1.0"
},
"engines": {
- "node": ">=10"
+ "node": ">=18"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
@@ -10804,67 +11367,141 @@
}
},
"node_modules/log-update": {
- "version": "4.0.0",
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz",
+ "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==",
"dev": true,
"license": "MIT",
"dependencies": {
- "ansi-escapes": "^4.3.0",
- "cli-cursor": "^3.1.0",
- "slice-ansi": "^4.0.0",
- "wrap-ansi": "^6.2.0"
+ "ansi-escapes": "^7.0.0",
+ "cli-cursor": "^5.0.0",
+ "slice-ansi": "^7.1.0",
+ "strip-ansi": "^7.1.0",
+ "wrap-ansi": "^9.0.0"
},
"engines": {
- "node": ">=10"
+ "node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/log-update/node_modules/ansi-regex": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
+ "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/log-update/node_modules/ansi-styles": {
+ "version": "6.2.3",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
+ "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
"node_modules/log-update/node_modules/emoji-regex": {
- "version": "8.0.0",
+ "version": "10.6.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
+ "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
"dev": true,
"license": "MIT"
},
+ "node_modules/log-update/node_modules/is-fullwidth-code-point": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz",
+ "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "get-east-asian-width": "^1.3.1"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/log-update/node_modules/slice-ansi": {
- "version": "4.0.0",
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz",
+ "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==",
"dev": true,
"license": "MIT",
"dependencies": {
- "ansi-styles": "^4.0.0",
- "astral-regex": "^2.0.0",
- "is-fullwidth-code-point": "^3.0.0"
+ "ansi-styles": "^6.2.1",
+ "is-fullwidth-code-point": "^5.0.0"
},
"engines": {
- "node": ">=10"
+ "node": ">=18"
},
"funding": {
"url": "https://github.com/chalk/slice-ansi?sponsor=1"
}
},
"node_modules/log-update/node_modules/string-width": {
- "version": "4.2.3",
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
+ "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "emoji-regex": "^8.0.0",
- "is-fullwidth-code-point": "^3.0.0",
- "strip-ansi": "^6.0.1"
+ "emoji-regex": "^10.3.0",
+ "get-east-asian-width": "^1.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/log-update/node_modules/strip-ansi": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
+ "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.2.2"
+ },
+ "engines": {
+ "node": ">=12"
},
- "engines": {
- "node": ">=8"
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/log-update/node_modules/wrap-ansi": {
- "version": "6.2.0",
+ "version": "9.0.2",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
+ "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
"dev": true,
"license": "MIT",
"dependencies": {
- "ansi-styles": "^4.0.0",
- "string-width": "^4.1.0",
- "strip-ansi": "^6.0.0"
+ "ansi-styles": "^6.2.1",
+ "string-width": "^7.0.0",
+ "strip-ansi": "^7.1.0"
},
"engines": {
- "node": ">=8"
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/loose-envify": {
@@ -10877,6 +11514,13 @@
"loose-envify": "cli.js"
}
},
+ "node_modules/loupe": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
+ "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/lru-cache": {
"version": "5.1.1",
"dev": true,
@@ -10973,6 +11617,21 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/merge-anything": {
+ "version": "5.1.7",
+ "resolved": "https://registry.npmjs.org/merge-anything/-/merge-anything-5.1.7.tgz",
+ "integrity": "sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-what": "^4.1.8"
+ },
+ "engines": {
+ "node": ">=12.13"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mesqueeb"
+ }
+ },
"node_modules/merge-descriptors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
@@ -11009,7 +11668,6 @@
},
"node_modules/methods": {
"version": "1.1.2",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
@@ -11214,6 +11872,12 @@
"node": ">= 0.6"
}
},
+ "node_modules/neo-async": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
+ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
+ "license": "MIT"
+ },
"node_modules/node-fetch": {
"version": "2.7.0",
"dev": true,
@@ -11924,11 +12588,23 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/pathval": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz",
+ "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14.16"
+ }
+ },
"node_modules/pause": {
"version": "0.0.1"
},
"node_modules/pend": {
"version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
+ "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
"dev": true,
"license": "MIT"
},
@@ -11938,6 +12614,8 @@
},
"node_modules/performance-now": {
"version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
+ "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
"dev": true,
"license": "MIT"
},
@@ -12205,9 +12883,9 @@
"license": "MIT"
},
"node_modules/qs": {
- "version": "6.14.2",
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
- "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
+ "version": "6.15.2",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
+ "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.1.0"
@@ -12415,13 +13093,15 @@
}
},
"node_modules/raw-body": {
- "version": "2.5.2",
+ "version": "2.5.3",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
+ "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
"license": "MIT",
"dependencies": {
- "bytes": "3.1.2",
- "http-errors": "2.0.0",
- "iconv-lite": "0.4.24",
- "unpipe": "1.0.0"
+ "bytes": "~3.1.2",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
@@ -12552,6 +13232,12 @@
"node": ">= 6"
}
},
+ "node_modules/reflect-metadata": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
+ "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==",
+ "license": "Apache-2.0"
+ },
"node_modules/reflect.getprototypeof": {
"version": "1.0.10",
"dev": true,
@@ -12678,15 +13364,49 @@
}
},
"node_modules/restore-cursor": {
- "version": "3.1.0",
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz",
+ "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "onetime": "^5.1.0",
- "signal-exit": "^3.0.2"
+ "onetime": "^7.0.0",
+ "signal-exit": "^4.1.0"
},
"engines": {
- "node": ">=8"
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/restore-cursor/node_modules/onetime": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz",
+ "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mimic-function": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/restore-cursor/node_modules/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/rfdc": {
@@ -13183,16 +13903,49 @@
}
},
"node_modules/slice-ansi": {
- "version": "3.0.0",
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz",
+ "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "ansi-styles": "^4.0.0",
- "astral-regex": "^2.0.0",
- "is-fullwidth-code-point": "^3.0.0"
+ "ansi-styles": "^6.2.3",
+ "is-fullwidth-code-point": "^5.1.0"
},
"engines": {
- "node": ">=8"
+ "node": ">=20"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/slice-ansi?sponsor=1"
+ }
+ },
+ "node_modules/slice-ansi/node_modules/ansi-styles": {
+ "version": "6.2.3",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
+ "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz",
+ "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "get-east-asian-width": "^1.3.1"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/smart-buffer": {
@@ -13218,7 +13971,6 @@
},
"node_modules/source-map": {
"version": "0.6.1",
- "dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
@@ -13302,6 +14054,8 @@
},
"node_modules/sshpk": {
"version": "1.18.0",
+ "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz",
+ "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -13332,7 +14086,9 @@
"license": "MIT"
},
"node_modules/statuses": {
- "version": "2.0.1",
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
@@ -13589,9 +14345,9 @@
}
},
"node_modules/strip-literal": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.0.0.tgz",
- "integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==",
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz",
+ "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -13865,9 +14621,9 @@
}
},
"node_modules/tinyspy": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.3.tgz",
- "integrity": "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==",
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz",
+ "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==",
"dev": true,
"license": "MIT",
"engines": {
@@ -13876,6 +14632,8 @@
},
"node_modules/tldts": {
"version": "6.1.86",
+ "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz",
+ "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -13887,11 +14645,15 @@
},
"node_modules/tldts-core": {
"version": "6.1.86",
+ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz",
+ "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==",
"dev": true,
"license": "MIT"
},
"node_modules/tmp": {
- "version": "0.2.5",
+ "version": "0.2.7",
+ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz",
+ "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -13919,6 +14681,8 @@
},
"node_modules/tough-cookie": {
"version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
+ "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
@@ -13958,6 +14722,15 @@
"typescript": ">=4.8.4"
}
},
+ "node_modules/ts-deepmerge": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/ts-deepmerge/-/ts-deepmerge-7.0.3.tgz",
+ "integrity": "sha512-Du/ZW2RfwV/D4cmA5rXafYjBQVuvu4qGiEEla4EmEHVHgRdx68Gftx7i66jn2bzHPwSVZY36Ae6OuDn9el4ZKA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=14.13.1"
+ }
+ },
"node_modules/ts-node": {
"version": "10.9.2",
"dev": true,
@@ -14034,6 +14807,23 @@
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
},
+ "node_modules/tsoa": {
+ "version": "7.0.0-alpha.0",
+ "resolved": "https://registry.npmjs.org/tsoa/-/tsoa-7.0.0-alpha.0.tgz",
+ "integrity": "sha512-o5h2DD1IKa2GF728BHYDL2uSX57a44sX8BLFScR4KbD0xlOmgsSDECneJmouDxf9MJdjHHWc+I1S3sGK82B6kQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@tsoa/cli": "^7.0.0-alpha.0",
+ "@tsoa/runtime": "^7.0.0-alpha.0"
+ },
+ "bin": {
+ "tsoa": "dist/cli.js"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "yarn": ">=1.9.4"
+ }
+ },
"node_modules/tsscmp": {
"version": "1.0.6",
"license": "MIT",
@@ -14063,6 +14853,8 @@
},
"node_modules/tunnel-agent": {
"version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
+ "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@@ -14205,7 +14997,6 @@
},
"node_modules/typescript": {
"version": "5.9.3",
- "dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
@@ -14247,6 +15038,19 @@
"node": ">=8"
}
},
+ "node_modules/uglify-js": {
+ "version": "3.19.3",
+ "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz",
+ "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==",
+ "license": "BSD-2-Clause",
+ "optional": true,
+ "bin": {
+ "uglifyjs": "bin/uglifyjs"
+ },
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
"node_modules/uid-safe": {
"version": "2.1.5",
"license": "MIT",
@@ -14314,7 +15118,6 @@
},
"node_modules/universalify": {
"version": "2.0.1",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 10.0.0"
@@ -14617,20 +15420,20 @@
}
},
"node_modules/vitest": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz",
- "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.6.tgz",
+ "integrity": "sha512-xejya+bT/j/+R/AGa1XOfRxLmNUlLtlwjRsFUILF+xHfzElmGcmFydy2gqqIrd62ptIEfwVMofd19uNWD9L7Nw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/chai": "^5.2.2",
- "@vitest/expect": "3.2.4",
- "@vitest/mocker": "3.2.4",
- "@vitest/pretty-format": "^3.2.4",
- "@vitest/runner": "3.2.4",
- "@vitest/snapshot": "3.2.4",
- "@vitest/spy": "3.2.4",
- "@vitest/utils": "3.2.4",
+ "@vitest/expect": "3.2.6",
+ "@vitest/mocker": "3.2.6",
+ "@vitest/pretty-format": "^3.2.6",
+ "@vitest/runner": "3.2.6",
+ "@vitest/snapshot": "3.2.6",
+ "@vitest/spy": "3.2.6",
+ "@vitest/utils": "3.2.6",
"chai": "^5.2.0",
"debug": "^4.4.1",
"expect-type": "^1.2.1",
@@ -14660,8 +15463,8 @@
"@edge-runtime/vm": "*",
"@types/debug": "^4.1.12",
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
- "@vitest/browser": "3.2.4",
- "@vitest/ui": "3.2.4",
+ "@vitest/browser": "3.2.6",
+ "@vitest/ui": "3.2.6",
"happy-dom": "*",
"jsdom": "*"
},
@@ -14689,107 +15492,6 @@
}
}
},
- "node_modules/vitest/node_modules/@types/chai": {
- "version": "5.2.2",
- "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz",
- "integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/deep-eql": "*"
- }
- },
- "node_modules/vitest/node_modules/@vitest/mocker": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz",
- "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@vitest/spy": "3.2.4",
- "estree-walker": "^3.0.3",
- "magic-string": "^0.30.17"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- },
- "peerDependencies": {
- "msw": "^2.4.9",
- "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
- },
- "peerDependenciesMeta": {
- "msw": {
- "optional": true
- },
- "vite": {
- "optional": true
- }
- }
- },
- "node_modules/vitest/node_modules/assertion-error": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
- "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/vitest/node_modules/chai": {
- "version": "5.3.3",
- "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
- "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "assertion-error": "^2.0.1",
- "check-error": "^2.1.1",
- "deep-eql": "^5.0.1",
- "loupe": "^3.1.0",
- "pathval": "^2.0.0"
- },
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/vitest/node_modules/check-error": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz",
- "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 16"
- }
- },
- "node_modules/vitest/node_modules/deep-eql": {
- "version": "5.0.2",
- "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
- "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/vitest/node_modules/loupe": {
- "version": "3.2.1",
- "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
- "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/vitest/node_modules/pathval": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz",
- "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 14.16"
- }
- },
"node_modules/vitest/node_modules/tinyexec": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
@@ -14936,7 +15638,6 @@
},
"node_modules/wordwrap": {
"version": "1.0.0",
- "dev": true,
"license": "MIT"
},
"node_modules/wordwrapjs": {
@@ -15087,7 +15788,6 @@
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz",
"integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==",
- "dev": true,
"license": "ISC",
"bin": {
"yaml": "bin.mjs"
@@ -15141,12 +15841,16 @@
}
},
"node_modules/yauzl": {
- "version": "2.10.0",
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.3.2.tgz",
+ "integrity": "sha512-Md9ankxxN23wncAN8s7+Tn3Co52zLUPMtnrLAbVCnfG5d2tKBFfmygYSgXlqFgXObtzIgqkx7aNgDBpso9+4qA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "buffer-crc32": "~0.2.3",
- "fd-slicer": "~1.1.0"
+ "pend": "~1.2.0"
+ },
+ "engines": {
+ "node": ">=12"
}
},
"node_modules/yn": {
diff --git a/package.json b/package.json
index 049bfcb51..4f465bd1b 100644
--- a/package.json
+++ b/package.json
@@ -59,10 +59,11 @@
"server": "cross-env ALLOWED_ORIGINS=* tsx index.ts",
"start": "concurrently \"npm run server\" \"npm run client\"",
"build": "npm run generate-config-types && npm run build-ui && npm run build-ts",
- "build-ts": "tsc --project tsconfig.publish.json && node scripts/fix-shebang.js",
+ "build-ts": "npm run build-tsoa && tsc --project tsconfig.publish.json && node scripts/fix-shebang.js",
"build-ui": "vite build",
+ "build-tsoa": "tsoa spec-and-routes",
"check-types": "tsc",
- "check-types:server": "tsc --project tsconfig.publish.json --noEmit",
+ "check-types:server": "npm run build-tsoa && tsc --project tsconfig.publish.json --noEmit",
"test-shuffle": "NODE_ENV=test vitest --run --dir ./test --sequence.shuffle",
"test": "cross-env NODE_ENV=test vitest --run --dir ./test",
"test:e2e": "vitest run --config vitest.config.e2e.ts",
@@ -75,13 +76,14 @@
"prepare": "node ./scripts/prepare.js",
"lint": "eslint",
"lint:fix": "eslint --fix",
+ "format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,md,yml,yaml,css,scss}\" --ignore-path .gitignore --ignore-path .prettierignore --config ./.prettierrc",
+ "format:check": "prettier --check \"**/*.{js,jsx,ts,tsx,json,md,yml,yaml,css,scss}\" --ignore-path .gitignore --ignore-path .prettierignore --config ./.prettierrc",
"migrate:urls": "node scripts/migrate/migrate-urls.js",
"migrate:users": "node scripts/migrate/migrate-users.js",
"backup:urls": "node scripts/migrate/backup-urls.js",
"backup:users": "node scripts/migrate/backup-users.js",
- "format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,md,yml,yaml,css,scss}\" --ignore-path .gitignore --config ./.prettierrc",
- "format:check": "prettier --check \"**/*.{js,jsx,ts,tsx,json,md,yml,yaml,css,scss}\" --ignore-path .gitignore --config ./.prettierrc",
"gen-schema-doc": "node ./scripts/doc-schema.js",
+ "gen-swagger-doc": "node ./scripts/doc-swagger.js",
"cypress:run": "cypress run",
"cypress:run:docker": "cypress run --config specPattern='cypress/e2e/docker/**/*.cy.{js,ts}'",
"cypress:open": "cypress open",
@@ -148,6 +150,7 @@
"react-router-dom": "6.30.3",
"simple-git": "^3.30.0",
"ssh2": "~1.17.0",
+ "tsoa": "^7.0.0-alpha.0",
"uuid": "^13.0.0",
"validator": "^13.15.26",
"yargs": "^17.7.2"
@@ -234,6 +237,9 @@
],
"test/**/*.{js,jsx,ts,tsx,json}": [
"eslint --fix"
+ ],
+ "{src/service/controllers/**/*Controller.ts,src/service/authentication.ts,src/service/generatedRoutes.ts,src/app.ts,tsoa.json}": [
+ "node scripts/check-tsoa-routes.js --"
]
}
}
diff --git a/scripts/check-tsoa-routes.js b/scripts/check-tsoa-routes.js
new file mode 100644
index 000000000..acb3b979d
--- /dev/null
+++ b/scripts/check-tsoa-routes.js
@@ -0,0 +1,38 @@
+#!/usr/bin/env node
+
+/**
+ * Copyright 2026 GitProxy Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * Invoked by lint-staged when any TSOA input is staged. Regenerates
+ * src/service/generatedRoutes.ts via `npm run build-tsoa` and the API
+ * docs under website/docs/api/ via `npm run gen-swagger-doc`, then stages
+ * both so the commit always includes routes and docs in sync with their
+ * inputs — the same fix-then-continue behaviour as `prettier --write`.
+ */
+const { spawnSync } = require('node:child_process');
+
+const run = (command) => {
+ const result = spawnSync(command, { stdio: 'inherit', shell: true });
+ if (result.status !== 0) {
+ process.exit(result.status ?? 1);
+ }
+};
+
+run('npm run --silent build-tsoa');
+run('git add src/service/generatedRoutes.ts');
+run('npm run --silent gen-swagger-doc');
+run('git add website/docs/api');
diff --git a/scripts/doc-swagger.js b/scripts/doc-swagger.js
new file mode 100644
index 000000000..c5f08259e
--- /dev/null
+++ b/scripts/doc-swagger.js
@@ -0,0 +1,362 @@
+/**
+ * Copyright 2026 GitProxy Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const { readFileSync, writeFileSync, mkdirSync } = require('fs');
+const { join } = require('path');
+
+const SWAGGER_FILE = './dist/swagger.json';
+const OUTPUT_DIR = './website/docs/api';
+
+const METHOD_COLORS = {
+ get: '#61affe',
+ post: '#49cc90',
+ put: '#fca130',
+ patch: '#50e3c2',
+ delete: '#f93e3e',
+};
+
+const STATUS_DESCRIPTIONS = {
+ 200: 'OK',
+ 201: 'Created',
+ 204: 'No Content',
+ 400: 'Bad Request',
+ 401: 'Unauthorized',
+ 403: 'Forbidden',
+ 404: 'Not Found',
+ 409: 'Conflict',
+ 500: 'Internal Server Error',
+};
+
+function loadSpec() {
+ const raw = readFileSync(SWAGGER_FILE, 'utf-8');
+ return JSON.parse(raw);
+}
+
+function httpMethodOrder(method) {
+ const order = { get: 0, post: 1, put: 2, patch: 3, delete: 4 };
+ return order[method] ?? 5;
+}
+
+function resolveRef(ref, spec) {
+ const path = ref.replace('#/', '').split('/');
+ let current = spec;
+ for (const segment of path) {
+ current = current[segment];
+ if (!current) return null;
+ }
+ return current;
+}
+
+function resolveSchema(schema, spec) {
+ if (!schema) return null;
+ if (schema.$ref) return resolveRef(schema.$ref, spec);
+ return schema;
+}
+
+function formatType(schema, spec) {
+ if (!schema) return '`any`';
+ const resolved = resolveSchema(schema, spec);
+ if (!resolved) return '`any`';
+
+ if (resolved.type === 'array') {
+ const itemType = resolved.items ? formatType(resolved.items, spec) : '`any`';
+ return `${itemType}[]`;
+ }
+
+ if (schema.$ref) {
+ const name = schema.$ref.split('/').pop();
+ return `\`${name}\``;
+ }
+
+ if (resolved.enum) {
+ return resolved.enum.map((v) => `\`"${v}"\``).join(' \\| ');
+ }
+
+ return `\`${resolved.type || 'object'}\``;
+}
+
+function methodBadge(method) {
+ const color = METHOD_COLORS[method] || '#999';
+ const label = method.toUpperCase();
+ return `${label}`;
+}
+
+function authBadge() {
+ return `AUTH`;
+}
+
+function statusBadge(code) {
+ let color = '#49cc90';
+ if (code >= 400 && code < 500) color = '#fca130';
+ if (code >= 500) color = '#f93e3e';
+ if (code >= 300 && code < 400) color = '#61affe';
+ return `${code}`;
+}
+
+function renderSchemaProperties(schema, spec) {
+ const resolved = resolveSchema(schema, spec);
+ if (!resolved || !resolved.properties) return '';
+
+ const required = new Set(resolved.required || []);
+ let lines = [];
+
+ lines.push('| Field | Type | Required | Description |');
+ lines.push('|-------|------|:--------:|-------------|');
+
+ for (const [name, prop] of Object.entries(resolved.properties)) {
+ const propResolved = resolveSchema(prop, spec);
+ const type = formatType(prop, spec);
+ const isRequired = required.has(name) ? '**Yes**' : 'No';
+ const desc = (propResolved?.description || '').replace(/\\/g, '\\\\').replace(/\|/g, '\\|');
+ lines.push(`| \`${name}\` | ${type} | ${isRequired} | ${desc} |`);
+ }
+
+ return lines.join('\n');
+}
+
+function renderParameters(parameters, spec) {
+ if (!parameters || parameters.length === 0) return '';
+
+ const params = parameters.map((p) => (p.$ref ? resolveRef(p.$ref, spec) : p)).filter(Boolean);
+ if (params.length === 0) return '';
+
+ let lines = [];
+ lines.push('#### Parameters\n');
+ lines.push('| Name | In | Type | Required | Description |');
+ lines.push('|------|:---:|------|:--------:|-------------|');
+
+ for (const param of params) {
+ const type = formatType(param.schema, spec);
+ const required = param.required ? '**Yes**' : 'No';
+ const desc = (param.description || '').replace(/\\/g, '\\\\').replace(/\|/g, '\\|');
+ const inBadge = param.in === 'path' ? '`path`' : '`query`';
+ lines.push(`| \`${param.name}\` | ${inBadge} | ${type} | ${required} | ${desc} |`);
+ }
+
+ return lines.join('\n');
+}
+
+function renderRequestBody(requestBody, spec) {
+ if (!requestBody) return '';
+
+ const content = requestBody.content;
+ if (!content) return '';
+
+ const jsonContent = content['application/json'];
+ if (!jsonContent || !jsonContent.schema) return '';
+
+ const schema = resolveSchema(jsonContent.schema, spec);
+ if (!schema) return '';
+
+ let lines = ['#### Request Body\n'];
+ const table = renderSchemaProperties(jsonContent.schema, spec);
+ if (table) {
+ lines.push(table);
+ } else {
+ lines.push(`Type: ${formatType(jsonContent.schema, spec)}`);
+ }
+ return lines.join('\n');
+}
+
+function renderResponses(responses) {
+ if (!responses) return '';
+
+ let lines = ['#### Responses\n'];
+
+ for (const [status, response] of Object.entries(responses)) {
+ const code = parseInt(status, 10);
+ const badge = statusBadge(code);
+ const desc = response.description || STATUS_DESCRIPTIONS[code] || '';
+ lines.push(`- ${badge} ${desc}`);
+ }
+
+ return lines.join('\n');
+}
+
+function renderSecurityNote(security) {
+ if (!security || security.length === 0) return '';
+ return ':::info[Authorization Required]\nThis endpoint requires a valid **JWT Bearer token** in the `Authorization` header.\n:::';
+}
+
+function countEndpoints(operations) {
+ const methods = {};
+ for (const op of operations) {
+ const m = op.method.toUpperCase();
+ methods[m] = (methods[m] || 0) + 1;
+ }
+ return Object.entries(methods)
+ .map(([m, c]) => `${c} ${m}`)
+ .join(', ');
+}
+
+function generateTagPage(tag, operations, spec) {
+ let lines = [];
+
+ lines.push('---');
+ lines.push(`title: ${tag}`);
+ lines.push(`description: ${tag} API endpoints for GitProxy`);
+ lines.push('---');
+ lines.push('');
+ lines.push(`# ${tag}\n`);
+
+ const tagDesc = (spec.tags || []).find((t) => t.name === tag);
+ if (tagDesc && tagDesc.description) {
+ lines.push(`${tagDesc.description}\n`);
+ }
+
+ lines.push(
+ `*${operations.length} endpoint${operations.length !== 1 ? 's' : ''} — ${countEndpoints(operations)}*\n`,
+ );
+
+ operations.sort((a, b) => {
+ const methodDiff = httpMethodOrder(a.method) - httpMethodOrder(b.method);
+ if (methodDiff !== 0) return methodDiff;
+ return a.path.localeCompare(b.path);
+ });
+
+ for (const op of operations) {
+ const badge = methodBadge(op.method);
+ const auth = op.security && op.security.length > 0 ? authBadge() : '';
+
+ lines.push(`## ${badge} \`${op.path}\`${auth}\n`);
+
+ if (op.description) {
+ lines.push(`${op.description}\n`);
+ } else if (op.summary) {
+ lines.push(`${op.summary}\n`);
+ }
+
+ const secNote = renderSecurityNote(op.security);
+ if (secNote) {
+ lines.push(`${secNote}\n`);
+ }
+
+ const params = renderParameters(op.parameters, spec);
+ if (params) {
+ lines.push(`${params}\n`);
+ }
+
+ const body = renderRequestBody(op.requestBody, spec);
+ if (body) {
+ lines.push(`${body}\n`);
+ }
+
+ const responses = renderResponses(op.responses);
+ if (responses) {
+ lines.push(`${responses}\n`);
+ }
+
+ lines.push('---\n');
+ }
+
+ return lines.join('\n');
+}
+
+function generateIndexPage(spec, tagList, tagOperations) {
+ const info = spec.info || {};
+ let lines = [];
+
+ lines.push('---');
+ lines.push('title: API Reference');
+ lines.push('description: REST API reference documentation for GitProxy');
+ lines.push('---');
+ lines.push('');
+ lines.push('# API Reference\n');
+
+ if (info.description) {
+ lines.push(`${info.description}\n`);
+ }
+
+ lines.push(`:::info[API Version]\n**${info.version || 'unknown'}** — OpenAPI 3.0\n:::\n`);
+
+ const securitySchemes = spec.components?.securitySchemes;
+ if (securitySchemes) {
+ lines.push('## Authentication\n');
+ for (const [name, scheme] of Object.entries(securitySchemes)) {
+ if (scheme.type === 'http') {
+ lines.push(
+ `Most endpoints require a **${scheme.scheme.toUpperCase()}** token${scheme.bearerFormat ? ` (${scheme.bearerFormat})` : ''} passed via the \`Authorization\` header:\n`,
+ );
+ lines.push('```\nAuthorization: Bearer \n```\n');
+ } else {
+ lines.push(`- **${name}**: ${scheme.type}\n`);
+ }
+ }
+ }
+
+ lines.push('## Endpoints\n');
+
+ const totalEndpoints = Object.values(tagOperations).reduce((sum, ops) => sum + ops.length, 0);
+ lines.push(`${totalEndpoints} endpoints across ${tagList.length} groups:\n`);
+
+ for (const tag of tagList) {
+ const slug = tag.toLowerCase().replace(/\s+/g, '-');
+ const ops = tagOperations[tag];
+ const summary = countEndpoints(ops);
+ lines.push(`- [**${tag}**](/docs/api/${slug}) — ${summary}`);
+ }
+
+ lines.push('');
+ return lines.join('\n');
+}
+
+function main() {
+ let spec;
+ try {
+ spec = loadSpec();
+ } catch (err) {
+ console.error(`Failed to read ${SWAGGER_FILE}: ${err.message}`);
+ console.error('Run "npm run build-tsoa" first to generate the OpenAPI spec.');
+ process.exit(1);
+ }
+
+ const tagOperations = {};
+
+ for (const [path, methods] of Object.entries(spec.paths || {})) {
+ for (const [method, operation] of Object.entries(methods)) {
+ if (['parameters', 'summary', 'description'].includes(method)) continue;
+
+ const tags = operation.tags || ['Default'];
+ for (const tag of tags) {
+ if (!tagOperations[tag]) tagOperations[tag] = [];
+ tagOperations[tag].push({
+ path,
+ method,
+ ...operation,
+ });
+ }
+ }
+ }
+
+ mkdirSync(OUTPUT_DIR, { recursive: true });
+
+ const tagList = Object.keys(tagOperations).sort();
+
+ const indexContent = generateIndexPage(spec, tagList, tagOperations);
+ writeFileSync(join(OUTPUT_DIR, 'index.mdx'), indexContent);
+ console.log(`Wrote ${join(OUTPUT_DIR, 'index.mdx')}`);
+
+ for (const [tag, operations] of Object.entries(tagOperations)) {
+ const slug = tag.toLowerCase().replace(/\s+/g, '-');
+ const content = generateTagPage(tag, operations, spec);
+ writeFileSync(join(OUTPUT_DIR, `${slug}.mdx`), content);
+ console.log(`Wrote ${join(OUTPUT_DIR, `${slug}.mdx`)}`);
+ }
+
+ console.log(`\nGenerated API docs for ${tagList.length} tag(s): ${tagList.join(', ')}`);
+}
+
+main();
diff --git a/src/service/routes/healthcheck.ts b/src/app.ts
similarity index 74%
rename from src/service/routes/healthcheck.ts
rename to src/app.ts
index bdc79aa2e..6ad0f79c4 100644
--- a/src/service/routes/healthcheck.ts
+++ b/src/app.ts
@@ -14,14 +14,8 @@
* limitations under the License.
*/
-import express, { Request, Response } from 'express';
+import express from 'express';
-const router = express.Router();
-
-router.get('/', (_req: Request, res: Response) => {
- res.send({
- message: 'ok',
- });
-});
-
-export default router;
+// Minimal entry point used by tsoa for controller/spec discovery.
+// The actual configured Express app lives in src/service/index.ts.
+export const app = express();
diff --git a/src/service/authentication.ts b/src/service/authentication.ts
new file mode 100644
index 000000000..c0b4e4489
--- /dev/null
+++ b/src/service/authentication.ts
@@ -0,0 +1,93 @@
+/**
+ * Copyright 2026 GitProxy Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import type { Request } from 'express';
+import { getAPIAuthMethods } from '../config';
+import { assignRoles, validateJwt } from './passport/jwtUtils';
+import type { RoleMapping } from '../config/generated/config';
+
+/**
+ * tsoa authentication handler called for every route decorated with @Security.
+ *
+ * Supported security names:
+ * - 'jwt': Bearer-token validation via OIDC JWT or an existing session.
+ */
+export async function expressAuthentication(
+ request: Request,
+ securityName: string,
+ _scopes?: string[],
+): Promise {
+ if (securityName === 'jwt') {
+ // Already authenticated via session (e.g. passport local login)
+ if (request.isAuthenticated && request.isAuthenticated()) {
+ return request.user;
+ }
+
+ const apiAuthMethods = getAPIAuthMethods();
+ const jwtAuthMethod = apiAuthMethods.find((m) => m.type.toLowerCase() === 'jwt');
+
+ if (!jwtAuthMethod || !jwtAuthMethod.enabled) {
+ // JWT not configured — pass through (other middleware may enforce auth)
+ return;
+ }
+
+ const token = request.header('Authorization');
+ if (!token) {
+ throw Object.assign(new Error('No token provided'), { status: 401 });
+ }
+
+ if (!jwtAuthMethod.jwtConfig) {
+ console.log('JWT configuration is missing');
+ throw Object.assign(new Error('JWT configuration is missing'), { status: 500 });
+ }
+
+ const { clientID, authorityURL, expectedAudience, roleMapping } = jwtAuthMethod.jwtConfig;
+
+ if (!authorityURL) {
+ console.log('OIDC authority URL is not configured');
+ throw Object.assign(new Error('OIDC authority URL is not configured'), { status: 500 });
+ }
+
+ if (!clientID) {
+ console.log('OIDC client ID is not configured');
+ throw Object.assign(new Error('OIDC client ID is not configured'), { status: 500 });
+ }
+
+ const audience = expectedAudience || clientID;
+ const tokenParts = token.split(' ');
+ const accessToken = tokenParts.length === 2 ? tokenParts[1] : tokenParts[0];
+
+ const { verifiedPayload, error } = await validateJwt(
+ accessToken,
+ authorityURL,
+ audience,
+ clientID,
+ );
+
+ if (error || !verifiedPayload) {
+ console.log('JWT validation failed');
+ throw Object.assign(new Error(error || 'JWT validation failed'), { status: 401 });
+ }
+
+ request.user = verifiedPayload;
+ assignRoles(roleMapping as RoleMapping, verifiedPayload, request.user);
+
+ console.log('JWT validation successful');
+ return verifiedPayload;
+ }
+
+ throw Object.assign(new Error(`Unknown security scheme: ${securityName}`), { status: 401 });
+}
diff --git a/src/service/controllers/AuthController.ts b/src/service/controllers/AuthController.ts
new file mode 100644
index 000000000..4e7254971
--- /dev/null
+++ b/src/service/controllers/AuthController.ts
@@ -0,0 +1,318 @@
+/**
+ * Copyright 2026 GitProxy Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import type { Request as ExpressRequest, Response as ExpressResponse, NextFunction } from 'express';
+import { Body, Controller, Get, Middlewares, Post, Request, Res, Route, Tags } from 'tsoa';
+import { getPassport, authStrategies } from '../passport';
+import { getAuthMethods, getUIHost, getUIPort } from '../../config';
+import * as db from '../../db';
+import * as passportLocal from '../passport/local';
+import * as passportAD from '../passport/activeDirectory';
+import { User } from '../../db/types';
+import { AuthenticationElement } from '../../config/generated/config';
+import { isAdminUser, toPublicUser } from '../routes/utils';
+import { handleErrorAndLog } from '../../utils/errors';
+import { PublicUser } from '../../db/types';
+import {
+ AuthResources,
+ AuthConfigResponse,
+ LoginResponse,
+ LogoutResponse,
+ CreateUserResponse,
+ CsrfTokenResponse,
+ GitAccountBody,
+ CreateUserBody,
+} from '../interfaces/auth.interfaces';
+import {
+ ForbiddenResponse,
+ InternalServerErrorResponse,
+ NotFoundResponse,
+ UnauthorisedResponse,
+ ValidationErrorResponse,
+} from '../decorators/response.types';
+
+// login strategies that will work with /login e.g. take username and password
+const appropriateLoginStrategies = [passportLocal.type, passportAD.type];
+
+// getLoginStrategy fetches the enabled auth methods and identifies if there's an appropriate
+// auth method for username and password login. If there isn't it returns null, if there is it
+// returns the first.
+const getLoginStrategy = () => {
+ // returns only enabled auth methods
+ // returns at least one enabled auth method
+ const enabledAppropriateLoginStrategies = getAuthMethods().filter((am: AuthenticationElement) =>
+ appropriateLoginStrategies.includes(am.type.toLowerCase()),
+ );
+ // for where no login strategies which work for /login are enabled
+ // just return null
+ if (enabledAppropriateLoginStrategies.length === 0) {
+ return null;
+ }
+ // return the first enabled auth method
+ return enabledAppropriateLoginStrategies[0].type.toLowerCase();
+};
+
+/**
+ * Dynamically selects the login passport strategy and runs it as Express middleware.
+ * Used by `@Middlewares` on the POST /login route.
+ */
+export function dynamicLoginMiddleware(
+ req: ExpressRequest,
+ res: ExpressResponse,
+ next: NextFunction,
+): void {
+ const authType = getLoginStrategy();
+ if (authType === null) {
+ res.status(403).send('Username and Password based Login is not enabled at this time').end();
+ return;
+ }
+ getPassport().authenticate(authType)(req, res, next);
+}
+
+/**
+ * Handles the OIDC callback: authenticates the user and redirects on success.
+ * Used by @Middlewares on the GET /openidconnect/callback route.
+ */
+export function oidcCallbackMiddleware(
+ req: ExpressRequest,
+ res: ExpressResponse,
+ next: NextFunction,
+): void {
+ getPassport().authenticate(
+ authStrategies['openidconnect'].type,
+ (err: unknown, user: Partial, info: unknown) => {
+ if (err) {
+ console.error('Authentication error:', err);
+ return res.status(500).end();
+ }
+ if (!user) {
+ console.error('No user found:', info);
+ return res.status(401).end();
+ }
+ req.logIn(user, (err) => {
+ if (err) {
+ console.error('Login error:', err);
+ return res.status(500).end();
+ }
+ console.log('Logged in successfully. User:', user);
+ return res.redirect(`${getUIHost()}:${getUIPort()}/dashboard/profile`);
+ });
+ },
+ )(req, res, next);
+}
+
+/**
+ * Authentication endpoints.
+ */
+@Route('api/auth')
+@Tags('Auth')
+export class AuthController extends Controller {
+ /**
+ * Returns links to the available authentication resource endpoints.
+ */
+ @Get('/')
+ public getResources(): AuthResources {
+ return {
+ login: { action: 'post', uri: '/api/auth/login' },
+ profile: { action: 'get', uri: '/api/auth/profile' },
+ logout: { action: 'post', uri: '/api/auth/logout' },
+ };
+ }
+
+ /**
+ * Returns the enabled authentication methods available to the UI.
+ */
+ @Get('/config')
+ public getAuthConfig(): AuthConfigResponse {
+ const usernamePasswordMethod = getLoginStrategy();
+ return {
+ // enabled username /password auth method
+ usernamePasswordMethod,
+ // other enabled auth methods
+ otherMethods: getAuthMethods()
+ .map((am) => am.type.toLowerCase())
+ .filter((authType) => authType !== usernamePasswordMethod),
+ };
+ }
+
+ /**
+ * Returns a CSRF token for the current session, to be sent with subsequent
+ * state-changing requests.
+ */
+ @Get('/csrf-token')
+ public getCsrfToken(@Request() req: ExpressRequest): CsrfTokenResponse {
+ console.log('req.user', req.user);
+ return { csrfToken: (req as ExpressRequest & { csrfToken: () => string }).csrfToken() };
+ }
+
+ /**
+ * Authenticates the user with a username/password strategy.
+ * The appropriate passport strategy is selected dynamically based on configuration.
+ */
+ @Post('/login')
+ @Middlewares(dynamicLoginMiddleware)
+ public async login(@Request() req: ExpressRequest): Promise {
+ // dynamicLoginMiddleware has already authenticated the user and set req.user.
+ // If strategy called next(), we can log in and return the user profile.
+ const user = req.user as User;
+
+ await new Promise((resolve, reject) => {
+ req.logIn(user, (err) => (err ? reject(err) : resolve()));
+ });
+
+ const currentUser = toPublicUser(user);
+ console.log(
+ `service.routes.auth.login: user logged in, username=${currentUser.username} profile=${JSON.stringify(currentUser)}`,
+ );
+ return { message: 'success', user: currentUser };
+ }
+
+ /**
+ * Initiates the OpenID Connect authentication flow (redirects to the OIDC provider).
+ * @hidden
+ */
+ @Get('/openidconnect')
+ @Middlewares(getPassport().authenticate(authStrategies['openidconnect'].type))
+ public initiateOIDC(): void {
+ // Passport middleware handles the redirect. This body is unreachable.
+ }
+
+ /**
+ * OpenID Connect callback — exchanges the authorization code for a session.
+ * @hidden
+ */
+ @Get('/openidconnect/callback')
+ @Middlewares(oidcCallbackMiddleware)
+ public handleOIDCCallback(): void {
+ // oidcCallbackMiddleware handles login and redirect. This body is unreachable.
+ }
+
+ /**
+ * Logs out the current user and clears the session cookie.
+ */
+ @Post('/logout')
+ public async logout(@Request() req: ExpressRequest): Promise {
+ await new Promise((resolve, reject) => {
+ req.logout((err: unknown) => (err ? reject(err) : resolve()));
+ });
+ req.res?.clearCookie('connect.sid');
+ return { isAuth: req.isAuthenticated(), user: req.user };
+ }
+
+ /**
+ * Returns the profile of the currently authenticated user.
+ */
+ @Get('/profile')
+ public async getProfile(
+ @Request() req: ExpressRequest,
+ @Res() unauthorisedResponse: UnauthorisedResponse,
+ @Res() notFoundResponse: NotFoundResponse,
+ ): Promise {
+ if (!req.user) {
+ return unauthorisedResponse(401, { message: 'Not logged in' });
+ }
+
+ const userVal = await db.findUser((req.user as User).username);
+ if (!userVal) {
+ return notFoundResponse(404, { message: 'User not found' });
+ }
+
+ return toPublicUser(userVal);
+ }
+
+ /**
+ * Updates the Git account (username) of a user.
+ * Admins may update any user; non-admins may only update their own account.
+ */
+ @Post('/gitAccount')
+ public async updateGitAccount(
+ @Body() body: GitAccountBody,
+ @Request() req: ExpressRequest,
+ @Res() unauthorisedResponse: UnauthorisedResponse,
+ @Res() notFoundResponse: NotFoundResponse,
+ @Res() validationErrorResponse: ValidationErrorResponse,
+ @Res() forbiddenResponse: ForbiddenResponse,
+ @Res() internalServerErrorResponse: InternalServerErrorResponse,
+ ): Promise {
+ if (!req.user) {
+ return unauthorisedResponse(401, { message: 'Not logged in' });
+ }
+
+ try {
+ let username =
+ body.username == null || body.username === 'undefined' ? body.id : body.username;
+ username = username?.split('@')[0];
+
+ if (!username) {
+ return validationErrorResponse(400, {
+ message: 'Missing username. Git account not updated',
+ });
+ }
+
+ const reqUser = await db.findUser((req.user as User).username);
+ if (username !== reqUser?.username && !reqUser?.admin) {
+ return forbiddenResponse(403, {
+ message: 'Must be an admin to update a different account',
+ });
+ }
+
+ const user = await db.findUser(username);
+ if (!user) {
+ return notFoundResponse(404, { message: 'User not found' });
+ }
+
+ user.gitAccount = body.gitAccount;
+ await db.updateUser(user);
+ this.setStatus(200);
+ } catch (error: unknown) {
+ const msg = handleErrorAndLog(error, 'Failed to update git account');
+ return internalServerErrorResponse(500, { message: msg });
+ }
+ }
+
+ /**
+ * Creates a new user. Requires admin privileges.
+ */
+ @Post('/create-user')
+ public async createUser(
+ @Body() body: CreateUserBody,
+ @Request() req: ExpressRequest,
+ @Res() unauthorisedResponse: ForbiddenResponse,
+ @Res() validationErrorResponse: ValidationErrorResponse,
+ @Res() internalServerErrorResponse: InternalServerErrorResponse,
+ ): Promise {
+ if (!isAdminUser(req.user)) {
+ return unauthorisedResponse(403, { message: 'Not authorized to create users' });
+ }
+
+ const { username, password, email, gitAccount, admin: isAdmin = false } = body;
+
+ if (!username || !password || !email || !gitAccount) {
+ return validationErrorResponse(400, {
+ message: 'Missing required fields: username, password, email, and gitAccount are required',
+ });
+ }
+
+ try {
+ await db.createUser(username, password, email, gitAccount, isAdmin);
+ this.setStatus(201);
+ return { message: 'User created successfully', username };
+ } catch (error: unknown) {
+ const msg = handleErrorAndLog(error, 'Failed to create user');
+ return internalServerErrorResponse(500, { message: msg });
+ }
+ }
+}
diff --git a/src/service/controllers/ConfigController.ts b/src/service/controllers/ConfigController.ts
new file mode 100644
index 000000000..8c0eb7abb
--- /dev/null
+++ b/src/service/controllers/ConfigController.ts
@@ -0,0 +1,51 @@
+/**
+ * Copyright 2026 GitProxy Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Controller, Get, Route, Tags } from 'tsoa';
+import * as config from '../../config';
+import type { AttestationConfig, SSH, UIRouteAuth } from '../../config/generated/config';
+
+/**
+ * Public configuration endpoints consumed by the UI.
+ */
+@Route('api/v1/config')
+@Tags('Config')
+export class ConfigController extends Controller {
+ @Get('/attestation')
+ public getAttestation(): AttestationConfig {
+ return config.getAttestationConfig();
+ }
+
+ @Get('/urlShortener')
+ public getUrlShortener(): string | undefined {
+ return config.getURLShortener();
+ }
+
+ @Get('/contactEmail')
+ public getContactEmail(): string | undefined {
+ return config.getContactEmail();
+ }
+
+ @Get('/uiRouteAuth')
+ public getUiRouteAuth(): UIRouteAuth {
+ return config.getUIRouteAuth();
+ }
+
+ @Get('/ssh')
+ public getSSH(): SSH {
+ return config.getSSHConfig();
+ }
+}
diff --git a/src/service/controllers/HealthController.ts b/src/service/controllers/HealthController.ts
new file mode 100644
index 000000000..76d8eac0b
--- /dev/null
+++ b/src/service/controllers/HealthController.ts
@@ -0,0 +1,29 @@
+/**
+ * Copyright 2026 GitProxy Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Controller, Get, Route, Tags } from 'tsoa';
+import { HealthResponse } from '../interfaces/health.interfaces';
+/**
+ * Service health check.
+ */
+@Route('api/v1/healthcheck')
+@Tags('Health')
+export class HealthController extends Controller {
+ @Get('/')
+ public check(): HealthResponse {
+ return { message: 'ok' };
+ }
+}
diff --git a/src/service/controllers/HomeController.ts b/src/service/controllers/HomeController.ts
new file mode 100644
index 000000000..3f003ba13
--- /dev/null
+++ b/src/service/controllers/HomeController.ts
@@ -0,0 +1,34 @@
+/**
+ * Copyright 2026 GitProxy Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Controller, Get, Route, Tags } from 'tsoa';
+import { ApiResources } from '../interfaces/home.interfaces';
+
+/**
+ * API home — lists available resource URIs.
+ */
+@Route('api')
+@Tags('Home')
+export class HomeController extends Controller {
+ @Get('/')
+ public getResources(): ApiResources {
+ return {
+ healthcheck: '/api/v1/healthcheck',
+ push: '/api/v1/push',
+ auth: '/api/auth',
+ };
+ }
+}
diff --git a/src/service/controllers/PushController.ts b/src/service/controllers/PushController.ts
new file mode 100644
index 000000000..313d931c7
--- /dev/null
+++ b/src/service/controllers/PushController.ts
@@ -0,0 +1,263 @@
+/**
+ * Copyright 2026 GitProxy Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Body, Controller, Get, Path, Post, Request, Res, Route, Security, Tags } from 'tsoa';
+import type { Request as ExpressRequest } from 'express';
+import * as db from '../../db';
+import { PushQuery } from '../../db/types';
+import { AttestationConfig } from '../../config/generated/config';
+import { getAttestationConfig } from '../../config';
+import { Action } from '../../proxy/actions/Action';
+import { AttestationAnswer, Rejection } from '../../proxy/processors/types';
+import { MessageResponse } from '../interfaces/common.interfaces';
+import { RejectBody, AuthoriseBody } from '../interfaces/push.interfaces';
+import {
+ ForbiddenResponse,
+ NotFoundResponse,
+ UnauthorisedResponse,
+ ValidationErrorResponse,
+} from '../decorators/response.types';
+
+/**
+ * Push request management.
+ */
+@Route('api/v1/push')
+@Security('jwt')
+@Tags('Push')
+export class PushController extends Controller {
+ /**
+ * Returns push requests, optionally filtered by query parameters.
+ * Supported filters: any field from PushQuery (error, blocked, allowPush, authorised, canceled, rejected, type).
+ */
+ @Get('/')
+ public async getPushes(@Request() req: ExpressRequest): Promise {
+ const query: Partial = { type: 'push' };
+
+ for (const key in req.query) {
+ if (!key) continue;
+ if (key === 'limit' || key === 'skip') continue;
+
+ const rawValue = req.query[key];
+ let parsedValue: boolean | undefined;
+ if (rawValue === 'false') parsedValue = false;
+ if (rawValue === 'true') parsedValue = true;
+ query[key] = parsedValue ?? rawValue?.toString();
+ }
+
+ return db.getPushes(query);
+ }
+
+ /**
+ * Returns a single push request by ID.
+ */
+ @Get('/{id}')
+ public async getPush(
+ @Path() id: string,
+ @Res() notFoundResponse: NotFoundResponse,
+ ): Promise {
+ const push = await db.getPush(id);
+ if (!push) {
+ return notFoundResponse(404, { message: 'not found' });
+ }
+ return push;
+ }
+
+ /**
+ * Rejects a pending push request.
+ */
+ @Post('/{id}/reject')
+ public async rejectPush(
+ @Path() id: string,
+ @Body() body: RejectBody,
+ @Request() req: ExpressRequest,
+ @Res() unauthorisedResponse: UnauthorisedResponse,
+ @Res() validationErrorResponse: ValidationErrorResponse,
+ @Res() notFoundResponse: NotFoundResponse,
+ @Res() forbiddenResponse: ForbiddenResponse,
+ ): Promise {
+ if (!req.user) {
+ return unauthorisedResponse(401, { message: 'Not logged in' });
+ }
+
+ const { reason } = body;
+ if (!reason || !reason.trim()) {
+ return validationErrorResponse(400, { message: 'Rejection reason is required' });
+ }
+
+ const { username } = req.user as { username: string };
+
+ const push = await db.getPush(id);
+ if (!push) {
+ return notFoundResponse(404, { message: 'Push request not found' });
+ }
+
+ if (!push.userEmail) {
+ return validationErrorResponse(400, { message: 'Push request has no user email' });
+ }
+
+ const committerEmail = push.userEmail;
+ const list = await db.getUsers({ email: committerEmail });
+
+ if (list.length === 0) {
+ return notFoundResponse(404, {
+ message: `No user found with the committer's email address: ${committerEmail}`,
+ });
+ }
+
+ if (list[0].username.toLowerCase() === username.toLowerCase() && !list[0].admin) {
+ return forbiddenResponse(403, { message: 'Cannot reject your own changes' });
+ }
+
+ const isAllowed = await db.canUserApproveRejectPush(id, username);
+ if (!isAllowed) {
+ return forbiddenResponse(403, {
+ message: `User ${username} is not authorised to reject changes on this project`,
+ });
+ }
+
+ const reviewerList = await db.getUsers({ username });
+ const reviewerEmail = reviewerList[0].email;
+
+ if (!reviewerEmail) {
+ return notFoundResponse(404, {
+ message: `There was no registered email address for the reviewer: ${username}`,
+ });
+ }
+
+ const rejection: Rejection = {
+ reason,
+ timestamp: new Date(),
+ reviewer: { username, email: reviewerEmail },
+ };
+
+ const result = await db.reject(id, rejection);
+ console.log(
+ `User ${username} rejected push request for ${id}${reason ? ` with reason: ${reason}` : ''}`,
+ );
+ return result;
+ }
+
+ /**
+ * Authorises (approves) a pending push request.
+ */
+ @Post('/{id}/authorise')
+ public async authorisePush(
+ @Path() id: string,
+ @Body() body: AuthoriseBody,
+ @Request() req: ExpressRequest,
+ @Res() unauthorisedResponse: UnauthorisedResponse,
+ @Res() validationErrorResponse: ValidationErrorResponse,
+ @Res() notFoundResponse: NotFoundResponse,
+ @Res() forbiddenResponse: ForbiddenResponse,
+ ): Promise {
+ if (!req.user) {
+ return unauthorisedResponse(401, { message: 'Not logged in' });
+ }
+
+ const answers = body.params.attestation;
+ if (!validateAttestation(answers, getAttestationConfig())) {
+ return validationErrorResponse(400, { message: 'Attestation is not complete' });
+ }
+
+ const { username } = req.user as { username: string };
+
+ const push = await db.getPush(id);
+ if (!push) {
+ return notFoundResponse(404, { message: 'Push request not found' });
+ }
+
+ // Get the committer of the push via their email address
+ const committerEmail = push.userEmail;
+ const list = await db.getUsers({ email: committerEmail });
+
+ if (list.length === 0) {
+ return notFoundResponse(404, {
+ message: `No user found with the committer's email address: ${committerEmail}`,
+ });
+ }
+
+ if (list[0].username.toLowerCase() === username.toLowerCase() && !list[0].admin) {
+ return forbiddenResponse(403, { message: 'Cannot approve your own changes' });
+ }
+
+ // If we are not the author, now check that we are allowed to authorise on this
+ // repo
+ const isAllowed = await db.canUserApproveRejectPush(id, username);
+ if (!isAllowed) {
+ return forbiddenResponse(403, {
+ message: `User ${username} not authorised to approve pushes on this project`,
+ });
+ }
+
+ const reviewerList = await db.getUsers({ username });
+ const reviewerEmail = reviewerList[0].email;
+
+ if (!reviewerEmail) {
+ return notFoundResponse(404, {
+ message: `There was no registered email address for the reviewer: ${username}`,
+ });
+ }
+
+ const attestation = {
+ answers,
+ timestamp: new Date(),
+ reviewer: { username, email: reviewerEmail },
+ };
+
+ console.log(`User ${username} approved push request for ${id}`);
+ return db.authorise(id, attestation);
+ }
+
+ /**
+ * Cancels a pending push request.
+ */
+ @Post('/{id}/cancel')
+ public async cancelPush(
+ @Path() id: string,
+ @Request() req: ExpressRequest,
+ @Res() unauthorisedResponse: UnauthorisedResponse,
+ @Res() forbiddenResponse: ForbiddenResponse,
+ ): Promise {
+ if (!req.user) {
+ return unauthorisedResponse(401, { message: 'Not logged in' });
+ }
+
+ const { username } = req.user as { username: string };
+ const isAllowed = await db.canUserCancelPush(id, username);
+
+ if (!isAllowed) {
+ console.log(`User ${username} not authorised to cancel push request for ${id}`);
+ return forbiddenResponse(403, {
+ message: `User ${username} not authorised to cancel push requests on this project`,
+ });
+ }
+
+ const result = await db.cancel(id);
+ console.log(`User ${username} canceled push request for ${id}`);
+ return result;
+ }
+}
+
+function validateAttestation(answers: AttestationAnswer[], config: AttestationConfig): boolean {
+ const configQuestions = config.questions ?? [];
+
+ if (!answers || answers.length !== configQuestions.length) {
+ return false;
+ }
+
+ const configLabels = new Set(configQuestions.map((q) => q.label));
+ return answers.every((answer) => configLabels.has(answer.label) && !!answer.checked);
+}
diff --git a/src/service/controllers/RepoController.ts b/src/service/controllers/RepoController.ts
new file mode 100644
index 000000000..d6551f465
--- /dev/null
+++ b/src/service/controllers/RepoController.ts
@@ -0,0 +1,293 @@
+/**
+ * Copyright 2026 GitProxy Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ Body,
+ Controller,
+ Delete,
+ Get,
+ Patch,
+ Path,
+ Post,
+ Request,
+ Res,
+ Route,
+ Security,
+ Tags,
+} from 'tsoa';
+import type { Request as ExpressRequest } from 'express';
+import * as db from '../../db';
+import { RepoQuery } from '../../db/types';
+import { getProxyURL } from '../urls';
+import { isAdminUser } from '../routes/utils';
+import { getProxy } from '../proxyStore';
+import { handleErrorAndLog } from '../../utils/errors';
+import { MessageResponse } from '../interfaces/common.interfaces';
+import { UsernameBody, CreateRepoBody, RepoWithProxy } from '../interfaces/repo.interfaces';
+import {
+ ConflictResponse,
+ InternalServerErrorResponse,
+ NotFoundResponse,
+ UnauthorisedResponse,
+ UserNotFoundResponse,
+ ValidationErrorResponse,
+} from '../decorators/response.types';
+
+/**
+ * Repository management.
+ */
+@Route('api/v1/repo')
+@Security('jwt')
+@Tags('Repositories')
+export class RepoController extends Controller {
+ /**
+ * Returns repositories, optionally filtered by query parameters.
+ */
+ @Get('/')
+ public async getRepos(@Request() req: ExpressRequest): Promise {
+ const proxyURL = getProxyURL(req);
+ const query: Partial = {};
+
+ for (const key in req.query) {
+ if (!key) continue;
+ if (key === 'limit' || key === 'skip') continue;
+
+ const rawValue = req.query[key];
+ let parsedValue: boolean | undefined;
+ if (rawValue === 'false') parsedValue = false;
+ if (rawValue === 'true') parsedValue = true;
+ query[key] = parsedValue ?? rawValue?.toString();
+ }
+
+ const repos = await db.getRepos(query);
+ return repos.map((d) => ({ ...d, proxyURL }));
+ }
+
+ /**
+ * Returns a single repository by ID.
+ */
+ @Get('/{id}')
+ public async getRepo(
+ @Path() id: string,
+ @Request() req: ExpressRequest,
+ @Res() notFoundResponse: NotFoundResponse,
+ ): Promise {
+ const proxyURL = getProxyURL(req);
+ const repo = await db.getRepoById(id);
+ if (!repo) {
+ return notFoundResponse(404, { message: `Repository ${id} not found` });
+ }
+ return { ...repo, proxyURL };
+ }
+
+ /**
+ * Creates a new repository. May restart the proxy if a new origin is added.
+ */
+ @Post('/')
+ public async createRepo(
+ @Body() body: CreateRepoBody,
+ @Request() req: ExpressRequest,
+ @Res() unauthorisedResponse: UnauthorisedResponse,
+ @Res() validationErrorResponse: ValidationErrorResponse,
+ @Res() conflictResponse: ConflictResponse,
+ @Res() internalServerErrorResponse: InternalServerErrorResponse,
+ ): Promise {
+ if (!isAdminUser(req.user)) {
+ return unauthorisedResponse(401, {
+ message: 'You are not authorised to perform this action.',
+ });
+ }
+
+ const repoUrl = body.url;
+
+ if (!repoUrl) {
+ return validationErrorResponse(400, { message: 'Repository url is required' });
+ }
+
+ const existing = await db.getRepoByUrl(repoUrl);
+ if (existing) {
+ return conflictResponse(409, { message: `Repository ${repoUrl} already exists!` });
+ }
+
+ try {
+ // figure out if this represent a new domain to proxy
+ let newOrigin = true;
+
+ const existingHosts = await db.getAllProxiedHosts();
+ existingHosts.forEach((h) => {
+ // assume SSL is in use and that our origins are missing the protocol
+ if (repoUrl.startsWith(`https://${h}`)) {
+ newOrigin = false;
+ }
+ });
+
+ console.log(
+ `API request to proxy repository ${repoUrl} is for a new origin: ${newOrigin},\n\texisting origin list was: ${JSON.stringify(existingHosts)}`,
+ );
+
+ const repoDetails = await db.createRepo(body);
+ const proxyURL = getProxyURL(req);
+
+ // restart the proxy if we're proxying a new domain
+ if (newOrigin) {
+ console.log('Restarting the proxy to handle an additional host');
+ const proxy = getProxy();
+ await proxy.stop();
+ await proxy.start();
+ }
+
+ return { ...repoDetails, proxyURL, message: 'created' };
+ } catch (error: unknown) {
+ const msg = handleErrorAndLog(error, 'Repository creation failed');
+ return internalServerErrorResponse(500, { message: msg });
+ }
+ }
+
+ /**
+ * Grants a user push permission on a repository.
+ */
+ @Patch('/{id}/user/push')
+ public async addPushUser(
+ @Path() id: string,
+ @Body() body: UsernameBody,
+ @Request() req: ExpressRequest,
+ @Res() unauthorisedResponse: UnauthorisedResponse,
+ @Res() userNotFoundResponse: UserNotFoundResponse,
+ ): Promise {
+ if (!isAdminUser(req.user)) {
+ return unauthorisedResponse(401, {
+ message: 'You are not authorised to perform this action.',
+ });
+ }
+
+ const username = body.username.toLowerCase();
+ const user = await db.findUser(username);
+ if (!user) {
+ return userNotFoundResponse(400, { error: 'User does not exist' });
+ }
+
+ await db.addUserCanPush(id, username);
+ return { message: 'created' };
+ }
+
+ /**
+ * Grants a user authorise permission on a repository.
+ */
+ @Patch('/{id}/user/authorise')
+ public async addAuthoriseUser(
+ @Path() id: string,
+ @Body() body: UsernameBody,
+ @Request() req: ExpressRequest,
+ @Res() unauthorisedResponse: UnauthorisedResponse,
+ @Res() userNotFoundResponse: UserNotFoundResponse,
+ ): Promise {
+ if (!isAdminUser(req.user)) {
+ return unauthorisedResponse(401, {
+ message: 'You are not authorised to perform this action.',
+ });
+ }
+
+ const user = await db.findUser(body.username);
+ if (!user) {
+ return userNotFoundResponse(400, { error: 'User does not exist' });
+ }
+
+ await db.addUserCanAuthorise(id, body.username);
+ return { message: 'created' };
+ }
+
+ /**
+ * Revokes a user's authorise permission on a repository.
+ */
+ @Delete('/{id}/user/authorise/{username}')
+ public async removeAuthoriseUser(
+ @Path() id: string,
+ @Path() username: string,
+ @Request() req: ExpressRequest,
+ @Res() unauthorisedResponse: UnauthorisedResponse,
+ @Res() userNotFoundResponse: UserNotFoundResponse,
+ ): Promise {
+ if (!isAdminUser(req.user)) {
+ return unauthorisedResponse(401, {
+ message: 'You are not authorised to perform this action.',
+ });
+ }
+
+ const user = await db.findUser(username);
+ if (!user) {
+ return userNotFoundResponse(400, { error: 'User does not exist' });
+ }
+
+ await db.removeUserCanAuthorise(id, username);
+ return { message: 'created' };
+ }
+
+ /**
+ * Revokes a user's push permission on a repository.
+ */
+ @Delete('/{id}/user/push/{username}')
+ public async removePushUser(
+ @Path() id: string,
+ @Path() username: string,
+ @Request() req: ExpressRequest,
+ @Res() unauthorisedResponse: UnauthorisedResponse,
+ @Res() userNotFoundResponse: UserNotFoundResponse,
+ ): Promise {
+ if (!isAdminUser(req.user)) {
+ return unauthorisedResponse(401, {
+ message: 'You are not authorised to perform this action.',
+ });
+ }
+
+ const user = await db.findUser(username);
+ if (!user) {
+ return userNotFoundResponse(400, { error: 'User does not exist' });
+ }
+
+ await db.removeUserCanPush(id, username);
+ return { message: 'created' };
+ }
+
+ /**
+ * Deletes a repository. May restart the proxy if a proxied host is removed.
+ */
+ @Delete('/{id}/delete')
+ public async deleteRepo(
+ @Path() id: string,
+ @Request() req: ExpressRequest,
+ @Res() unauthorisedResponse: UnauthorisedResponse,
+ ): Promise {
+ if (!isAdminUser(req.user)) {
+ return unauthorisedResponse(401, {
+ message: 'You are not authorised to perform this action.',
+ });
+ }
+
+ // determine if we need to restart the proxy
+ const previousHosts = await db.getAllProxiedHosts();
+ await db.deleteRepo(id);
+ const currentHosts = await db.getAllProxiedHosts();
+
+ if (currentHosts.length < previousHosts.length) {
+ console.log('Restarting the proxy to remove a host');
+ const proxy = getProxy();
+ await proxy.stop();
+ await proxy.start();
+ }
+
+ return { message: 'deleted' };
+ }
+}
diff --git a/src/service/controllers/UserController.ts b/src/service/controllers/UserController.ts
new file mode 100644
index 000000000..706ead87e
--- /dev/null
+++ b/src/service/controllers/UserController.ts
@@ -0,0 +1,258 @@
+/**
+ * Copyright 2026 GitProxy Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ Body,
+ Controller,
+ Delete,
+ Get,
+ Path,
+ Post,
+ Request,
+ Res,
+ Route,
+ Security,
+ Tags,
+} from 'tsoa';
+import type { Request as ExpressRequest } from 'express';
+import crypto from 'crypto';
+import * as db from '../../db';
+import { PublicUser } from '../../db/types';
+import { toPublicUser } from '../routes/utils';
+import {
+ AuthenticationRequiredResponse,
+ BadRequestErrorResponse,
+ ConflictErrorResponse,
+ ForbiddenErrorResponse,
+ NotFoundErrorResponse,
+ NotFoundResponse,
+ ServerErrorResponse,
+} from '../decorators/response.types';
+import {
+ AddSSHKeyBody,
+ AddSSHKeyResponse,
+ RemoveSSHKeyResponse,
+ SSHKeyFingerprint,
+} from '../interfaces/user.interfaces';
+
+// Calculate SHA-256 fingerprint from SSH public key
+// Note: This function is duplicated in src/cli/ssh-key.ts to keep CLI and server independent
+function calculateFingerprint(publicKeyStr: string): string | null {
+ try {
+ const { utils } = require('ssh2');
+ const parsed = utils.parseKey(publicKeyStr);
+ if (!parsed || parsed instanceof Error) {
+ return null;
+ }
+ const pubKey = parsed.getPublicSSH();
+ const hash = crypto.createHash('sha256').update(pubKey).digest('base64');
+ return `SHA256:${hash}`;
+ } catch (err) {
+ console.error('Error calculating fingerprint:', err);
+ return null;
+ }
+}
+
+/**
+ * User listing.
+ */
+@Route('api/v1/user')
+@Security('jwt')
+@Tags('Users')
+export class UserController extends Controller {
+ /**
+ * Returns all registered users (public fields only).
+ */
+ @Get('/')
+ public async getUsers(): Promise {
+ console.log('fetching users');
+ const users = await db.getUsers();
+ return users.map(toPublicUser);
+ }
+
+ /**
+ * Returns a single user by username.
+ */
+ @Get('/{id}')
+ public async getUser(
+ @Path() id: string,
+ @Res() notFoundResponse: NotFoundResponse,
+ ): Promise {
+ const username = id.toLowerCase();
+ console.log(`Retrieving details for user: ${username}`);
+ const user = await db.findUser(username);
+ if (!user) {
+ return notFoundResponse(404, { message: `User ${username} not found` });
+ }
+ return toPublicUser(user);
+ }
+
+ /**
+ * Returns the SSH key fingerprints for a user.
+ * Users may view their own keys; admins may view any user's keys.
+ */
+ @Get('/{username}/ssh-key-fingerprints')
+ public async getSshKeyFingerprints(
+ @Path() username: string,
+ @Request() req: ExpressRequest,
+ @Res() authRequiredResponse: AuthenticationRequiredResponse,
+ @Res() forbiddenResponse: ForbiddenErrorResponse,
+ @Res() serverErrorResponse: ServerErrorResponse,
+ ): Promise {
+ if (!req.user) {
+ return authRequiredResponse(401, { error: 'Authentication required' });
+ }
+
+ const { username: requesterUsername, admin } = req.user as {
+ username: string;
+ admin: boolean;
+ };
+ const targetUsername = username.toLowerCase();
+
+ // Only allow users to view their own keys, or admins to view any keys
+ if (requesterUsername !== targetUsername && !admin) {
+ return forbiddenResponse(403, { error: 'Not authorized to view keys for this user' });
+ }
+
+ try {
+ const publicKeys = await db.getPublicKeys(targetUsername);
+ return publicKeys.map((keyRecord) => ({
+ fingerprint: keyRecord.fingerprint,
+ name: keyRecord.name,
+ addedAt: keyRecord.addedAt,
+ }));
+ } catch (error) {
+ console.error('Error retrieving SSH keys:', error);
+ return serverErrorResponse(500, { error: 'Failed to retrieve SSH keys' });
+ }
+ }
+
+ /**
+ * Adds an SSH public key to a user's account.
+ * Users may add keys to their own account; admins may add to any account.
+ */
+ @Post('/{username}/ssh-keys')
+ public async addSshKey(
+ @Path() username: string,
+ @Body() body: AddSSHKeyBody,
+ @Request() req: ExpressRequest,
+ @Res() authRequiredResponse: AuthenticationRequiredResponse,
+ @Res() forbiddenResponse: ForbiddenErrorResponse,
+ @Res() badRequestResponse: BadRequestErrorResponse,
+ @Res() notFoundResponse: NotFoundErrorResponse,
+ @Res() conflictResponse: ConflictErrorResponse,
+ @Res() serverErrorResponse: ServerErrorResponse,
+ ): Promise {
+ if (!req.user) {
+ return authRequiredResponse(401, { error: 'Authentication required' });
+ }
+
+ const { username: requesterUsername, admin } = req.user as {
+ username: string;
+ admin: boolean;
+ };
+ const targetUsername = username.toLowerCase();
+
+ // Only allow users to add keys to their own account, or admins to add to any account
+ if (requesterUsername !== targetUsername && !admin) {
+ return forbiddenResponse(403, { error: 'Not authorized to add keys for this user' });
+ }
+
+ const { publicKey, name } = body;
+ if (!publicKey) {
+ return badRequestResponse(400, { error: 'Public key is required' });
+ }
+
+ // Strip the comment from the key (everything after the last space)
+ const keyWithoutComment = publicKey.trim().split(' ').slice(0, 2).join(' ');
+
+ // Calculate fingerprint
+ const fingerprint = calculateFingerprint(keyWithoutComment);
+ if (!fingerprint) {
+ return badRequestResponse(400, { error: 'Invalid SSH public key format' });
+ }
+
+ const publicKeyRecord = {
+ key: keyWithoutComment,
+ name: name || 'Unnamed Key',
+ addedAt: new Date().toISOString(),
+ fingerprint: fingerprint,
+ };
+
+ console.log('Adding SSH key', { targetUsername, fingerprint });
+ try {
+ await db.addPublicKey(targetUsername, publicKeyRecord);
+ this.setStatus(201);
+ return { message: 'SSH key added successfully', fingerprint };
+ } catch (error: unknown) {
+ console.error('Error adding SSH key:', error);
+
+ // Return specific error message
+ const message = error instanceof Error ? error.message : undefined;
+ if (message === 'SSH key already exists') {
+ return conflictResponse(409, { error: 'This SSH key already exists' });
+ } else if (message === 'User not found') {
+ return notFoundResponse(404, { error: 'User not found' });
+ }
+ return serverErrorResponse(500, { error: message || 'Failed to add SSH key' });
+ }
+ }
+
+ /**
+ * Removes an SSH public key from a user's account by fingerprint.
+ * Users may remove keys from their own account; admins may remove from any account.
+ */
+ @Delete('/{username}/ssh-keys/{fingerprint}')
+ public async removeSshKey(
+ @Path() username: string,
+ @Path() fingerprint: string,
+ @Request() req: ExpressRequest,
+ @Res() authRequiredResponse: AuthenticationRequiredResponse,
+ @Res() forbiddenResponse: ForbiddenErrorResponse,
+ @Res() notFoundResponse: NotFoundErrorResponse,
+ @Res() serverErrorResponse: ServerErrorResponse,
+ ): Promise {
+ if (!req.user) {
+ return authRequiredResponse(401, { error: 'Authentication required' });
+ }
+
+ const { username: requesterUsername, admin } = req.user as {
+ username: string;
+ admin: boolean;
+ };
+ const targetUsername = username.toLowerCase();
+
+ // Only allow users to remove keys from their own account, or admins to remove from any account
+ if (requesterUsername !== targetUsername && !admin) {
+ return forbiddenResponse(403, { error: 'Not authorized to remove keys for this user' });
+ }
+
+ console.log('Removing SSH key', { targetUsername, fingerprint });
+ try {
+ await db.removePublicKey(targetUsername, fingerprint);
+ return { message: 'SSH key removed successfully' };
+ } catch (error: unknown) {
+ console.error('Error removing SSH key:', error);
+
+ // Return specific error message
+ const message = error instanceof Error ? error.message : undefined;
+ if (message === 'User not found') {
+ return notFoundResponse(404, { error: 'User not found' });
+ }
+ return serverErrorResponse(500, { error: message || 'Failed to remove SSH key' });
+ }
+ }
+}
diff --git a/src/service/decorators/response.types.ts b/src/service/decorators/response.types.ts
new file mode 100644
index 000000000..9859aa6de
--- /dev/null
+++ b/src/service/decorators/response.types.ts
@@ -0,0 +1,33 @@
+/**
+ * Copyright 2026 GitProxy Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { TsoaResponse } from 'tsoa';
+
+export type UnauthorisedResponse = TsoaResponse<401, { message: string }>;
+export type ForbiddenResponse = TsoaResponse<403, { message: string }>;
+export type NotFoundResponse = TsoaResponse<404, { message: string }>;
+export type ValidationErrorResponse = TsoaResponse<400, { message: string }>;
+export type ConflictResponse = TsoaResponse<409, { message: string }>;
+export type InternalServerErrorResponse = TsoaResponse<500, { message: string }>;
+export type UserNotFoundResponse = TsoaResponse<400, { error: string }>;
+
+// Error responses using an `{ error: string }` body shape (consumed by the SSH key UI/CLI).
+export type AuthenticationRequiredResponse = TsoaResponse<401, { error: string }>;
+export type ForbiddenErrorResponse = TsoaResponse<403, { error: string }>;
+export type BadRequestErrorResponse = TsoaResponse<400, { error: string }>;
+export type NotFoundErrorResponse = TsoaResponse<404, { error: string }>;
+export type ConflictErrorResponse = TsoaResponse<409, { error: string }>;
+export type ServerErrorResponse = TsoaResponse<500, { error: string }>;
diff --git a/src/service/generatedRoutes.ts b/src/service/generatedRoutes.ts
new file mode 100644
index 000000000..870a523e9
--- /dev/null
+++ b/src/service/generatedRoutes.ts
@@ -0,0 +1,1620 @@
+/* tslint:disable */
+/* eslint-disable */
+// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+import type { TsoaRoute } from '@tsoa/runtime';
+import { fetchMiddlewares, ExpressTemplateService } from '@tsoa/runtime';
+// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+import { UserController } from './controllers/UserController';
+// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+import { RepoController } from './controllers/RepoController';
+// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+import { PushController } from './controllers/PushController';
+// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+import { HomeController } from './controllers/HomeController';
+// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+import { HealthController } from './controllers/HealthController';
+// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+import { ConfigController } from './controllers/ConfigController';
+// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+import { AuthController } from './controllers/AuthController';
+import { expressAuthentication } from './authentication';
+// @ts-ignore - no great way to install types from subpackage
+import type { Request as ExRequest, Response as ExResponse, RequestHandler, Router } from 'express';
+
+const expressAuthenticationRecasted = expressAuthentication as (req: ExRequest, securityName: string, scopes?: string[], res?: ExResponse) => Promise;
+
+
+// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+const models: TsoaRoute.Models = {
+ "PublicUser": {
+ "dataType": "refObject",
+ "properties": {
+ "username": {"dataType":"string","required":true},
+ "displayName": {"dataType":"string","required":true},
+ "email": {"dataType":"string","required":true},
+ "title": {"dataType":"string","required":true},
+ "gitAccount": {"dataType":"string","required":true},
+ "admin": {"dataType":"boolean","required":true},
+ },
+ "additionalProperties": false,
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "SSHKeyFingerprint": {
+ "dataType": "refObject",
+ "properties": {
+ "fingerprint": {"dataType":"string","required":true},
+ "name": {"dataType":"string","required":true},
+ "addedAt": {"dataType":"string","required":true},
+ },
+ "additionalProperties": false,
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "AddSSHKeyResponse": {
+ "dataType": "refObject",
+ "properties": {
+ "message": {"dataType":"string","required":true},
+ "fingerprint": {"dataType":"string","required":true},
+ },
+ "additionalProperties": false,
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "AddSSHKeyBody": {
+ "dataType": "refObject",
+ "properties": {
+ "publicKey": {"dataType":"string"},
+ "name": {"dataType":"string"},
+ },
+ "additionalProperties": false,
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "RemoveSSHKeyResponse": {
+ "dataType": "refObject",
+ "properties": {
+ "message": {"dataType":"string","required":true},
+ },
+ "additionalProperties": false,
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "RepoWithProxy": {
+ "dataType": "refObject",
+ "properties": {
+ "project": {"dataType":"string","required":true},
+ "name": {"dataType":"string","required":true},
+ "url": {"dataType":"string","required":true},
+ "users": {"dataType":"nestedObjectLiteral","nestedProperties":{"canAuthorise":{"dataType":"array","array":{"dataType":"string"},"required":true},"canPush":{"dataType":"array","array":{"dataType":"string"},"required":true}},"required":true},
+ "_id": {"dataType":"string"},
+ "proxyURL": {"dataType":"string","required":true},
+ },
+ "additionalProperties": false,
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "MessageResponse": {
+ "dataType": "refObject",
+ "properties": {
+ "message": {"dataType":"string","required":true},
+ },
+ "additionalProperties": false,
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "CreateRepoBody": {
+ "dataType": "refObject",
+ "properties": {
+ "url": {"dataType":"string","required":true},
+ "name": {"dataType":"string","required":true},
+ "project": {"dataType":"string","required":true},
+ "users": {"dataType":"nestedObjectLiteral","nestedProperties":{"canAuthorise":{"dataType":"array","array":{"dataType":"string"},"required":true},"canPush":{"dataType":"array","array":{"dataType":"string"},"required":true}}},
+ "_id": {"dataType":"string"},
+ },
+ "additionalProperties": false,
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "UsernameBody": {
+ "dataType": "refObject",
+ "properties": {
+ "username": {"dataType":"string","required":true},
+ },
+ "additionalProperties": false,
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "Step": {
+ "dataType": "refObject",
+ "properties": {
+ "id": {"dataType":"string","required":true},
+ "stepName": {"dataType":"string","required":true},
+ "content": {"dataType":"any","required":true},
+ "error": {"dataType":"boolean","required":true},
+ "errorMessage": {"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}],"required":true},
+ "blocked": {"dataType":"boolean","required":true},
+ "blockedMessage": {"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}],"required":true},
+ "logs": {"dataType":"array","array":{"dataType":"string"},"default":[]},
+ },
+ "additionalProperties": false,
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "CommitData": {
+ "dataType": "refAlias",
+ "type": {"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true},"commitTimestamp":{"dataType":"string","required":true},"committerEmail":{"dataType":"string","required":true},"authorEmail":{"dataType":"string","required":true},"committer":{"dataType":"string","required":true},"author":{"dataType":"string","required":true},"parent":{"dataType":"string","required":true},"tree":{"dataType":"string","required":true}},"validators":{}},
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "AttestationBase": {
+ "dataType": "refAlias",
+ "type": {"dataType":"nestedObjectLiteral","nestedProperties":{"automated":{"dataType":"boolean"},"timestamp":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"datetime"}],"required":true},"reviewer":{"dataType":"nestedObjectLiteral","nestedProperties":{"email":{"dataType":"string","required":true},"username":{"dataType":"string","required":true}},"required":true}},"validators":{}},
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "AttestationAnswer": {
+ "dataType": "refObject",
+ "properties": {
+ "label": {"dataType":"string","required":true},
+ "checked": {"dataType":"boolean","required":true},
+ },
+ "additionalProperties": false,
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "CompletedAttestation": {
+ "dataType": "refAlias",
+ "type": {"dataType":"intersection","subSchemas":[{"ref":"AttestationBase"},{"dataType":"nestedObjectLiteral","nestedProperties":{"answers":{"dataType":"array","array":{"dataType":"refObject","ref":"AttestationAnswer"},"required":true}}}],"validators":{}},
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "Rejection": {
+ "dataType": "refAlias",
+ "type": {"dataType":"intersection","subSchemas":[{"ref":"AttestationBase"},{"dataType":"nestedObjectLiteral","nestedProperties":{"reason":{"dataType":"string","required":true}}}],"validators":{}},
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "Action": {
+ "dataType": "refObject",
+ "properties": {
+ "id": {"dataType":"string","required":true},
+ "type": {"dataType":"string","required":true},
+ "method": {"dataType":"string","required":true},
+ "timestamp": {"dataType":"double","required":true},
+ "project": {"dataType":"string","required":true},
+ "repoName": {"dataType":"string","required":true},
+ "url": {"dataType":"string","required":true},
+ "repo": {"dataType":"string","required":true},
+ "steps": {"dataType":"array","array":{"dataType":"refObject","ref":"Step"},"default":[]},
+ "error": {"dataType":"boolean","default":false},
+ "errorMessage": {"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}]},
+ "blocked": {"dataType":"boolean","default":false},
+ "blockedMessage": {"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}]},
+ "allowPush": {"dataType":"boolean","default":false},
+ "authorised": {"dataType":"boolean","default":false},
+ "canceled": {"dataType":"boolean","default":false},
+ "rejected": {"dataType":"boolean","default":false},
+ "autoApproved": {"dataType":"boolean","default":false},
+ "autoRejected": {"dataType":"boolean","default":false},
+ "commitData": {"dataType":"array","array":{"dataType":"refAlias","ref":"CommitData"},"default":[]},
+ "commitFrom": {"dataType":"string"},
+ "commitTo": {"dataType":"string"},
+ "branch": {"dataType":"string"},
+ "message": {"dataType":"string"},
+ "author": {"dataType":"string"},
+ "user": {"dataType":"string"},
+ "userEmail": {"dataType":"string"},
+ "attestation": {"ref":"CompletedAttestation"},
+ "rejection": {"ref":"Rejection"},
+ "lastStep": {"ref":"Step"},
+ "proxyGitPath": {"dataType":"string"},
+ "newIdxFiles": {"dataType":"array","array":{"dataType":"string"}},
+ "protocol": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["https"]},{"dataType":"enum","enums":["ssh"]}]},
+ "pullAuthStrategy": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["basic"]},{"dataType":"enum","enums":["ssh-user-key"]},{"dataType":"enum","enums":["ssh-service-token"]},{"dataType":"enum","enums":["ssh-agent-forwarding"]},{"dataType":"enum","enums":["anonymous"]}]},
+ },
+ "additionalProperties": false,
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "RejectBody": {
+ "dataType": "refObject",
+ "properties": {
+ "reason": {"dataType":"string","required":true},
+ },
+ "additionalProperties": false,
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "AuthoriseBody": {
+ "dataType": "refObject",
+ "properties": {
+ "params": {"dataType":"nestedObjectLiteral","nestedProperties":{"attestation":{"dataType":"array","array":{"dataType":"refObject","ref":"AttestationAnswer"},"required":true}},"required":true},
+ },
+ "additionalProperties": false,
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "ApiResources": {
+ "dataType": "refObject",
+ "properties": {
+ "healthcheck": {"dataType":"string","required":true},
+ "push": {"dataType":"string","required":true},
+ "auth": {"dataType":"string","required":true},
+ },
+ "additionalProperties": false,
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "HealthResponse": {
+ "dataType": "refObject",
+ "properties": {
+ "message": {"dataType":"enum","enums":["ok"],"required":true},
+ },
+ "additionalProperties": false,
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "Link": {
+ "dataType": "refObject",
+ "properties": {
+ "text": {"dataType":"string","required":true},
+ "url": {"dataType":"string","required":true},
+ },
+ "additionalProperties": false,
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "QuestionTooltip": {
+ "dataType": "refObject",
+ "properties": {
+ "links": {"dataType":"array","array":{"dataType":"refObject","ref":"Link"}},
+ "text": {"dataType":"string","required":true},
+ },
+ "additionalProperties": false,
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "Question": {
+ "dataType": "refObject",
+ "properties": {
+ "label": {"dataType":"string","required":true},
+ "tooltip": {"ref":"QuestionTooltip","required":true},
+ },
+ "additionalProperties": false,
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "AttestationConfig": {
+ "dataType": "refObject",
+ "properties": {
+ "questions": {"dataType":"array","array":{"dataType":"refObject","ref":"Question"}},
+ },
+ "additionalProperties": false,
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "RouteAuthRule": {
+ "dataType": "refObject",
+ "properties": {
+ "adminOnly": {"dataType":"boolean"},
+ "loginRequired": {"dataType":"boolean"},
+ "pattern": {"dataType":"string"},
+ },
+ "additionalProperties": {"dataType":"any"},
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "UIRouteAuth": {
+ "dataType": "refObject",
+ "properties": {
+ "enabled": {"dataType":"boolean"},
+ "rules": {"dataType":"array","array":{"dataType":"refObject","ref":"RouteAuthRule"}},
+ },
+ "additionalProperties": {"dataType":"any"},
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "HostKey": {
+ "dataType": "refObject",
+ "properties": {
+ "privateKeyPath": {"dataType":"string","required":true},
+ "publicKeyPath": {"dataType":"string","required":true},
+ },
+ "additionalProperties": false,
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "SSH": {
+ "dataType": "refObject",
+ "properties": {
+ "agentForwardingErrorMessage": {"dataType":"string"},
+ "debug": {"dataType":"boolean"},
+ "enabled": {"dataType":"boolean","required":true},
+ "hostKey": {"ref":"HostKey"},
+ "knownHosts": {"dataType":"nestedObjectLiteral","nestedProperties":{},"additionalProperties":{"dataType":"string"}},
+ "port": {"dataType":"double"},
+ },
+ "additionalProperties": false,
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "AuthResources": {
+ "dataType": "refObject",
+ "properties": {
+ "login": {"dataType":"nestedObjectLiteral","nestedProperties":{"uri":{"dataType":"string","required":true},"action":{"dataType":"enum","enums":["post"],"required":true}},"required":true},
+ "profile": {"dataType":"nestedObjectLiteral","nestedProperties":{"uri":{"dataType":"string","required":true},"action":{"dataType":"enum","enums":["get"],"required":true}},"required":true},
+ "logout": {"dataType":"nestedObjectLiteral","nestedProperties":{"uri":{"dataType":"string","required":true},"action":{"dataType":"enum","enums":["post"],"required":true}},"required":true},
+ },
+ "additionalProperties": false,
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "AuthConfigResponse": {
+ "dataType": "refObject",
+ "properties": {
+ "usernamePasswordMethod": {"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}],"required":true},
+ "otherMethods": {"dataType":"array","array":{"dataType":"string"},"required":true},
+ },
+ "additionalProperties": false,
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "CsrfTokenResponse": {
+ "dataType": "refObject",
+ "properties": {
+ "csrfToken": {"dataType":"string","required":true},
+ },
+ "additionalProperties": false,
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "LoginResponse": {
+ "dataType": "refObject",
+ "properties": {
+ "message": {"dataType":"enum","enums":["success"],"required":true},
+ "user": {"ref":"PublicUser","required":true},
+ },
+ "additionalProperties": false,
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "Express.User": {
+ "dataType": "refObject",
+ "properties": {
+ },
+ "additionalProperties": false,
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "LogoutResponse": {
+ "dataType": "refObject",
+ "properties": {
+ "isAuth": {"dataType":"boolean","required":true},
+ "user": {"dataType":"union","subSchemas":[{"ref":"Express.User"},{"dataType":"undefined"}],"required":true},
+ },
+ "additionalProperties": false,
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "GitAccountBody": {
+ "dataType": "refObject",
+ "properties": {
+ "username": {"dataType":"string"},
+ "id": {"dataType":"string"},
+ "gitAccount": {"dataType":"string","required":true},
+ },
+ "additionalProperties": false,
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "CreateUserResponse": {
+ "dataType": "refObject",
+ "properties": {
+ "message": {"dataType":"string","required":true},
+ "username": {"dataType":"string","required":true},
+ },
+ "additionalProperties": false,
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ "CreateUserBody": {
+ "dataType": "refObject",
+ "properties": {
+ "username": {"dataType":"string","required":true},
+ "password": {"dataType":"string","required":true},
+ "email": {"dataType":"string","required":true},
+ "gitAccount": {"dataType":"string","required":true},
+ "admin": {"dataType":"boolean"},
+ },
+ "additionalProperties": false,
+ },
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+};
+const templateService = new ExpressTemplateService(models, {"noImplicitAdditionalProperties":"throw-on-extras","bodyCoercion":true});
+
+// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+
+
+
+export function RegisterRoutes(app: Router) {
+
+ // ###########################################################################################################
+ // NOTE: If you do not see routes for all of your controllers in this file, then you might not have informed tsoa of where to look
+ // Please look into the "controllerPathGlobs" config option described in the readme: https://github.com/lukeautry/tsoa
+ // ###########################################################################################################
+
+
+
+ const argsUserController_getUsers: Record = {
+ };
+ app.get('/api/v1/user',
+ authenticateMiddleware([{"jwt":[]}]),
+ ...(fetchMiddlewares(UserController)),
+ ...(fetchMiddlewares(UserController.prototype.getUsers)),
+
+ async function UserController_getUsers(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsUserController_getUsers, request, response });
+
+ const controller = new UserController();
+
+ await templateService.apiHandler({
+ methodName: 'getUsers',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsUserController_getUser: Record = {
+ id: {"in":"path","name":"id","required":true,"dataType":"string"},
+ notFoundResponse: {"in":"res","name":"404","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ };
+ app.get('/api/v1/user/:id',
+ authenticateMiddleware([{"jwt":[]}]),
+ ...(fetchMiddlewares(UserController)),
+ ...(fetchMiddlewares(UserController.prototype.getUser)),
+
+ async function UserController_getUser(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsUserController_getUser, request, response });
+
+ const controller = new UserController();
+
+ await templateService.apiHandler({
+ methodName: 'getUser',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsUserController_getSshKeyFingerprints: Record = {
+ username: {"in":"path","name":"username","required":true,"dataType":"string"},
+ req: {"in":"request","name":"req","required":true,"dataType":"object"},
+ authRequiredResponse: {"in":"res","name":"401","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"error":{"dataType":"string","required":true}}},
+ forbiddenResponse: {"in":"res","name":"403","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"error":{"dataType":"string","required":true}}},
+ serverErrorResponse: {"in":"res","name":"500","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"error":{"dataType":"string","required":true}}},
+ };
+ app.get('/api/v1/user/:username/ssh-key-fingerprints',
+ authenticateMiddleware([{"jwt":[]}]),
+ ...(fetchMiddlewares(UserController)),
+ ...(fetchMiddlewares(UserController.prototype.getSshKeyFingerprints)),
+
+ async function UserController_getSshKeyFingerprints(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsUserController_getSshKeyFingerprints, request, response });
+
+ const controller = new UserController();
+
+ await templateService.apiHandler({
+ methodName: 'getSshKeyFingerprints',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsUserController_addSshKey: Record = {
+ username: {"in":"path","name":"username","required":true,"dataType":"string"},
+ body: {"in":"body","name":"body","required":true,"ref":"AddSSHKeyBody"},
+ req: {"in":"request","name":"req","required":true,"dataType":"object"},
+ authRequiredResponse: {"in":"res","name":"401","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"error":{"dataType":"string","required":true}}},
+ forbiddenResponse: {"in":"res","name":"403","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"error":{"dataType":"string","required":true}}},
+ badRequestResponse: {"in":"res","name":"400","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"error":{"dataType":"string","required":true}}},
+ notFoundResponse: {"in":"res","name":"404","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"error":{"dataType":"string","required":true}}},
+ conflictResponse: {"in":"res","name":"409","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"error":{"dataType":"string","required":true}}},
+ serverErrorResponse: {"in":"res","name":"500","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"error":{"dataType":"string","required":true}}},
+ };
+ app.post('/api/v1/user/:username/ssh-keys',
+ authenticateMiddleware([{"jwt":[]}]),
+ ...(fetchMiddlewares(UserController)),
+ ...(fetchMiddlewares(UserController.prototype.addSshKey)),
+
+ async function UserController_addSshKey(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsUserController_addSshKey, request, response });
+
+ const controller = new UserController();
+
+ await templateService.apiHandler({
+ methodName: 'addSshKey',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsUserController_removeSshKey: Record = {
+ username: {"in":"path","name":"username","required":true,"dataType":"string"},
+ fingerprint: {"in":"path","name":"fingerprint","required":true,"dataType":"string"},
+ req: {"in":"request","name":"req","required":true,"dataType":"object"},
+ authRequiredResponse: {"in":"res","name":"401","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"error":{"dataType":"string","required":true}}},
+ forbiddenResponse: {"in":"res","name":"403","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"error":{"dataType":"string","required":true}}},
+ notFoundResponse: {"in":"res","name":"404","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"error":{"dataType":"string","required":true}}},
+ serverErrorResponse: {"in":"res","name":"500","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"error":{"dataType":"string","required":true}}},
+ };
+ app.delete('/api/v1/user/:username/ssh-keys/:fingerprint',
+ authenticateMiddleware([{"jwt":[]}]),
+ ...(fetchMiddlewares(UserController)),
+ ...(fetchMiddlewares(UserController.prototype.removeSshKey)),
+
+ async function UserController_removeSshKey(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsUserController_removeSshKey, request, response });
+
+ const controller = new UserController();
+
+ await templateService.apiHandler({
+ methodName: 'removeSshKey',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsRepoController_getRepos: Record = {
+ req: {"in":"request","name":"req","required":true,"dataType":"object"},
+ };
+ app.get('/api/v1/repo',
+ authenticateMiddleware([{"jwt":[]}]),
+ ...(fetchMiddlewares(RepoController)),
+ ...(fetchMiddlewares(RepoController.prototype.getRepos)),
+
+ async function RepoController_getRepos(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsRepoController_getRepos, request, response });
+
+ const controller = new RepoController();
+
+ await templateService.apiHandler({
+ methodName: 'getRepos',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsRepoController_getRepo: Record = {
+ id: {"in":"path","name":"id","required":true,"dataType":"string"},
+ req: {"in":"request","name":"req","required":true,"dataType":"object"},
+ notFoundResponse: {"in":"res","name":"404","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ };
+ app.get('/api/v1/repo/:id',
+ authenticateMiddleware([{"jwt":[]}]),
+ ...(fetchMiddlewares(RepoController)),
+ ...(fetchMiddlewares(RepoController.prototype.getRepo)),
+
+ async function RepoController_getRepo(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsRepoController_getRepo, request, response });
+
+ const controller = new RepoController();
+
+ await templateService.apiHandler({
+ methodName: 'getRepo',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsRepoController_createRepo: Record = {
+ body: {"in":"body","name":"body","required":true,"ref":"CreateRepoBody"},
+ req: {"in":"request","name":"req","required":true,"dataType":"object"},
+ unauthorisedResponse: {"in":"res","name":"401","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ validationErrorResponse: {"in":"res","name":"400","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ conflictResponse: {"in":"res","name":"409","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ internalServerErrorResponse: {"in":"res","name":"500","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ };
+ app.post('/api/v1/repo',
+ authenticateMiddleware([{"jwt":[]}]),
+ ...(fetchMiddlewares(RepoController)),
+ ...(fetchMiddlewares(RepoController.prototype.createRepo)),
+
+ async function RepoController_createRepo(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsRepoController_createRepo, request, response });
+
+ const controller = new RepoController();
+
+ await templateService.apiHandler({
+ methodName: 'createRepo',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsRepoController_addPushUser: Record = {
+ id: {"in":"path","name":"id","required":true,"dataType":"string"},
+ body: {"in":"body","name":"body","required":true,"ref":"UsernameBody"},
+ req: {"in":"request","name":"req","required":true,"dataType":"object"},
+ unauthorisedResponse: {"in":"res","name":"401","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ userNotFoundResponse: {"in":"res","name":"400","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"error":{"dataType":"string","required":true}}},
+ };
+ app.patch('/api/v1/repo/:id/user/push',
+ authenticateMiddleware([{"jwt":[]}]),
+ ...(fetchMiddlewares(RepoController)),
+ ...(fetchMiddlewares(RepoController.prototype.addPushUser)),
+
+ async function RepoController_addPushUser(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsRepoController_addPushUser, request, response });
+
+ const controller = new RepoController();
+
+ await templateService.apiHandler({
+ methodName: 'addPushUser',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsRepoController_addAuthoriseUser: Record = {
+ id: {"in":"path","name":"id","required":true,"dataType":"string"},
+ body: {"in":"body","name":"body","required":true,"ref":"UsernameBody"},
+ req: {"in":"request","name":"req","required":true,"dataType":"object"},
+ unauthorisedResponse: {"in":"res","name":"401","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ userNotFoundResponse: {"in":"res","name":"400","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"error":{"dataType":"string","required":true}}},
+ };
+ app.patch('/api/v1/repo/:id/user/authorise',
+ authenticateMiddleware([{"jwt":[]}]),
+ ...(fetchMiddlewares(RepoController)),
+ ...(fetchMiddlewares(RepoController.prototype.addAuthoriseUser)),
+
+ async function RepoController_addAuthoriseUser(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsRepoController_addAuthoriseUser, request, response });
+
+ const controller = new RepoController();
+
+ await templateService.apiHandler({
+ methodName: 'addAuthoriseUser',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsRepoController_removeAuthoriseUser: Record = {
+ id: {"in":"path","name":"id","required":true,"dataType":"string"},
+ username: {"in":"path","name":"username","required":true,"dataType":"string"},
+ req: {"in":"request","name":"req","required":true,"dataType":"object"},
+ unauthorisedResponse: {"in":"res","name":"401","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ userNotFoundResponse: {"in":"res","name":"400","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"error":{"dataType":"string","required":true}}},
+ };
+ app.delete('/api/v1/repo/:id/user/authorise/:username',
+ authenticateMiddleware([{"jwt":[]}]),
+ ...(fetchMiddlewares(RepoController)),
+ ...(fetchMiddlewares(RepoController.prototype.removeAuthoriseUser)),
+
+ async function RepoController_removeAuthoriseUser(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsRepoController_removeAuthoriseUser, request, response });
+
+ const controller = new RepoController();
+
+ await templateService.apiHandler({
+ methodName: 'removeAuthoriseUser',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsRepoController_removePushUser: Record = {
+ id: {"in":"path","name":"id","required":true,"dataType":"string"},
+ username: {"in":"path","name":"username","required":true,"dataType":"string"},
+ req: {"in":"request","name":"req","required":true,"dataType":"object"},
+ unauthorisedResponse: {"in":"res","name":"401","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ userNotFoundResponse: {"in":"res","name":"400","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"error":{"dataType":"string","required":true}}},
+ };
+ app.delete('/api/v1/repo/:id/user/push/:username',
+ authenticateMiddleware([{"jwt":[]}]),
+ ...(fetchMiddlewares(RepoController)),
+ ...(fetchMiddlewares(RepoController.prototype.removePushUser)),
+
+ async function RepoController_removePushUser(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsRepoController_removePushUser, request, response });
+
+ const controller = new RepoController();
+
+ await templateService.apiHandler({
+ methodName: 'removePushUser',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsRepoController_deleteRepo: Record = {
+ id: {"in":"path","name":"id","required":true,"dataType":"string"},
+ req: {"in":"request","name":"req","required":true,"dataType":"object"},
+ unauthorisedResponse: {"in":"res","name":"401","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ };
+ app.delete('/api/v1/repo/:id/delete',
+ authenticateMiddleware([{"jwt":[]}]),
+ ...(fetchMiddlewares(RepoController)),
+ ...(fetchMiddlewares(RepoController.prototype.deleteRepo)),
+
+ async function RepoController_deleteRepo(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsRepoController_deleteRepo, request, response });
+
+ const controller = new RepoController();
+
+ await templateService.apiHandler({
+ methodName: 'deleteRepo',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsPushController_getPushes: Record = {
+ req: {"in":"request","name":"req","required":true,"dataType":"object"},
+ };
+ app.get('/api/v1/push',
+ authenticateMiddleware([{"jwt":[]}]),
+ ...(fetchMiddlewares(PushController)),
+ ...(fetchMiddlewares(PushController.prototype.getPushes)),
+
+ async function PushController_getPushes(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsPushController_getPushes, request, response });
+
+ const controller = new PushController();
+
+ await templateService.apiHandler({
+ methodName: 'getPushes',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsPushController_getPush: Record = {
+ id: {"in":"path","name":"id","required":true,"dataType":"string"},
+ notFoundResponse: {"in":"res","name":"404","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ };
+ app.get('/api/v1/push/:id',
+ authenticateMiddleware([{"jwt":[]}]),
+ ...(fetchMiddlewares(PushController)),
+ ...(fetchMiddlewares(PushController.prototype.getPush)),
+
+ async function PushController_getPush(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsPushController_getPush, request, response });
+
+ const controller = new PushController();
+
+ await templateService.apiHandler({
+ methodName: 'getPush',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsPushController_rejectPush: Record = {
+ id: {"in":"path","name":"id","required":true,"dataType":"string"},
+ body: {"in":"body","name":"body","required":true,"ref":"RejectBody"},
+ req: {"in":"request","name":"req","required":true,"dataType":"object"},
+ unauthorisedResponse: {"in":"res","name":"401","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ validationErrorResponse: {"in":"res","name":"400","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ notFoundResponse: {"in":"res","name":"404","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ forbiddenResponse: {"in":"res","name":"403","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ };
+ app.post('/api/v1/push/:id/reject',
+ authenticateMiddleware([{"jwt":[]}]),
+ ...(fetchMiddlewares(PushController)),
+ ...(fetchMiddlewares(PushController.prototype.rejectPush)),
+
+ async function PushController_rejectPush(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsPushController_rejectPush, request, response });
+
+ const controller = new PushController();
+
+ await templateService.apiHandler({
+ methodName: 'rejectPush',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsPushController_authorisePush: Record = {
+ id: {"in":"path","name":"id","required":true,"dataType":"string"},
+ body: {"in":"body","name":"body","required":true,"ref":"AuthoriseBody"},
+ req: {"in":"request","name":"req","required":true,"dataType":"object"},
+ unauthorisedResponse: {"in":"res","name":"401","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ validationErrorResponse: {"in":"res","name":"400","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ notFoundResponse: {"in":"res","name":"404","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ forbiddenResponse: {"in":"res","name":"403","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ };
+ app.post('/api/v1/push/:id/authorise',
+ authenticateMiddleware([{"jwt":[]}]),
+ ...(fetchMiddlewares(PushController)),
+ ...(fetchMiddlewares(PushController.prototype.authorisePush)),
+
+ async function PushController_authorisePush(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsPushController_authorisePush, request, response });
+
+ const controller = new PushController();
+
+ await templateService.apiHandler({
+ methodName: 'authorisePush',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsPushController_cancelPush: Record = {
+ id: {"in":"path","name":"id","required":true,"dataType":"string"},
+ req: {"in":"request","name":"req","required":true,"dataType":"object"},
+ unauthorisedResponse: {"in":"res","name":"401","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ forbiddenResponse: {"in":"res","name":"403","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ };
+ app.post('/api/v1/push/:id/cancel',
+ authenticateMiddleware([{"jwt":[]}]),
+ ...(fetchMiddlewares(PushController)),
+ ...(fetchMiddlewares(PushController.prototype.cancelPush)),
+
+ async function PushController_cancelPush(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsPushController_cancelPush, request, response });
+
+ const controller = new PushController();
+
+ await templateService.apiHandler({
+ methodName: 'cancelPush',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsHomeController_getResources: Record = {
+ };
+ app.get('/api',
+ ...(fetchMiddlewares(HomeController)),
+ ...(fetchMiddlewares(HomeController.prototype.getResources)),
+
+ async function HomeController_getResources(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsHomeController_getResources, request, response });
+
+ const controller = new HomeController();
+
+ await templateService.apiHandler({
+ methodName: 'getResources',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsHealthController_check: Record = {
+ };
+ app.get('/api/v1/healthcheck',
+ ...(fetchMiddlewares(HealthController)),
+ ...(fetchMiddlewares(HealthController.prototype.check)),
+
+ async function HealthController_check(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsHealthController_check, request, response });
+
+ const controller = new HealthController();
+
+ await templateService.apiHandler({
+ methodName: 'check',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsConfigController_getAttestation: Record = {
+ };
+ app.get('/api/v1/config/attestation',
+ ...(fetchMiddlewares(ConfigController)),
+ ...(fetchMiddlewares(ConfigController.prototype.getAttestation)),
+
+ async function ConfigController_getAttestation(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsConfigController_getAttestation, request, response });
+
+ const controller = new ConfigController();
+
+ await templateService.apiHandler({
+ methodName: 'getAttestation',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsConfigController_getUrlShortener: Record = {
+ };
+ app.get('/api/v1/config/urlShortener',
+ ...(fetchMiddlewares(ConfigController)),
+ ...(fetchMiddlewares(ConfigController.prototype.getUrlShortener)),
+
+ async function ConfigController_getUrlShortener(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsConfigController_getUrlShortener, request, response });
+
+ const controller = new ConfigController();
+
+ await templateService.apiHandler({
+ methodName: 'getUrlShortener',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsConfigController_getContactEmail: Record = {
+ };
+ app.get('/api/v1/config/contactEmail',
+ ...(fetchMiddlewares(ConfigController)),
+ ...(fetchMiddlewares(ConfigController.prototype.getContactEmail)),
+
+ async function ConfigController_getContactEmail(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsConfigController_getContactEmail, request, response });
+
+ const controller = new ConfigController();
+
+ await templateService.apiHandler({
+ methodName: 'getContactEmail',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsConfigController_getUiRouteAuth: Record = {
+ };
+ app.get('/api/v1/config/uiRouteAuth',
+ ...(fetchMiddlewares(ConfigController)),
+ ...(fetchMiddlewares(ConfigController.prototype.getUiRouteAuth)),
+
+ async function ConfigController_getUiRouteAuth(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsConfigController_getUiRouteAuth, request, response });
+
+ const controller = new ConfigController();
+
+ await templateService.apiHandler({
+ methodName: 'getUiRouteAuth',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsConfigController_getSSH: Record = {
+ };
+ app.get('/api/v1/config/ssh',
+ ...(fetchMiddlewares(ConfigController)),
+ ...(fetchMiddlewares(ConfigController.prototype.getSSH)),
+
+ async function ConfigController_getSSH(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsConfigController_getSSH, request, response });
+
+ const controller = new ConfigController();
+
+ await templateService.apiHandler({
+ methodName: 'getSSH',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsAuthController_getResources: Record = {
+ };
+ app.get('/api/auth',
+ ...(fetchMiddlewares(AuthController)),
+ ...(fetchMiddlewares(AuthController.prototype.getResources)),
+
+ async function AuthController_getResources(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsAuthController_getResources, request, response });
+
+ const controller = new AuthController();
+
+ await templateService.apiHandler({
+ methodName: 'getResources',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsAuthController_getAuthConfig: Record = {
+ };
+ app.get('/api/auth/config',
+ ...(fetchMiddlewares(AuthController)),
+ ...(fetchMiddlewares(AuthController.prototype.getAuthConfig)),
+
+ async function AuthController_getAuthConfig(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsAuthController_getAuthConfig, request, response });
+
+ const controller = new AuthController();
+
+ await templateService.apiHandler({
+ methodName: 'getAuthConfig',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsAuthController_getCsrfToken: Record = {
+ req: {"in":"request","name":"req","required":true,"dataType":"object"},
+ };
+ app.get('/api/auth/csrf-token',
+ ...(fetchMiddlewares(AuthController)),
+ ...(fetchMiddlewares(AuthController.prototype.getCsrfToken)),
+
+ async function AuthController_getCsrfToken(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsAuthController_getCsrfToken, request, response });
+
+ const controller = new AuthController();
+
+ await templateService.apiHandler({
+ methodName: 'getCsrfToken',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsAuthController_login: Record = {
+ req: {"in":"request","name":"req","required":true,"dataType":"object"},
+ };
+ app.post('/api/auth/login',
+ ...(fetchMiddlewares(AuthController)),
+ ...(fetchMiddlewares(AuthController.prototype.login)),
+
+ async function AuthController_login(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsAuthController_login, request, response });
+
+ const controller = new AuthController();
+
+ await templateService.apiHandler({
+ methodName: 'login',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsAuthController_initiateOIDC: Record = {
+ };
+ app.get('/api/auth/openidconnect',
+ ...(fetchMiddlewares(AuthController)),
+ ...(fetchMiddlewares(AuthController.prototype.initiateOIDC)),
+
+ async function AuthController_initiateOIDC(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsAuthController_initiateOIDC, request, response });
+
+ const controller = new AuthController();
+
+ await templateService.apiHandler({
+ methodName: 'initiateOIDC',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsAuthController_handleOIDCCallback: Record = {
+ };
+ app.get('/api/auth/openidconnect/callback',
+ ...(fetchMiddlewares(AuthController)),
+ ...(fetchMiddlewares(AuthController.prototype.handleOIDCCallback)),
+
+ async function AuthController_handleOIDCCallback(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsAuthController_handleOIDCCallback, request, response });
+
+ const controller = new AuthController();
+
+ await templateService.apiHandler({
+ methodName: 'handleOIDCCallback',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsAuthController_logout: Record = {
+ req: {"in":"request","name":"req","required":true,"dataType":"object"},
+ };
+ app.post('/api/auth/logout',
+ ...(fetchMiddlewares(AuthController)),
+ ...(fetchMiddlewares(AuthController.prototype.logout)),
+
+ async function AuthController_logout(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsAuthController_logout, request, response });
+
+ const controller = new AuthController();
+
+ await templateService.apiHandler({
+ methodName: 'logout',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsAuthController_getProfile: Record = {
+ req: {"in":"request","name":"req","required":true,"dataType":"object"},
+ unauthorisedResponse: {"in":"res","name":"401","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ notFoundResponse: {"in":"res","name":"404","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ };
+ app.get('/api/auth/profile',
+ ...(fetchMiddlewares(AuthController)),
+ ...(fetchMiddlewares(AuthController.prototype.getProfile)),
+
+ async function AuthController_getProfile(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsAuthController_getProfile, request, response });
+
+ const controller = new AuthController();
+
+ await templateService.apiHandler({
+ methodName: 'getProfile',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsAuthController_updateGitAccount: Record = {
+ body: {"in":"body","name":"body","required":true,"ref":"GitAccountBody"},
+ req: {"in":"request","name":"req","required":true,"dataType":"object"},
+ unauthorisedResponse: {"in":"res","name":"401","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ notFoundResponse: {"in":"res","name":"404","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ validationErrorResponse: {"in":"res","name":"400","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ forbiddenResponse: {"in":"res","name":"403","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ internalServerErrorResponse: {"in":"res","name":"500","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ };
+ app.post('/api/auth/gitAccount',
+ ...(fetchMiddlewares(AuthController)),
+ ...(fetchMiddlewares(AuthController.prototype.updateGitAccount)),
+
+ async function AuthController_updateGitAccount(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsAuthController_updateGitAccount, request, response });
+
+ const controller = new AuthController();
+
+ await templateService.apiHandler({
+ methodName: 'updateGitAccount',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ const argsAuthController_createUser: Record = {
+ body: {"in":"body","name":"body","required":true,"ref":"CreateUserBody"},
+ req: {"in":"request","name":"req","required":true,"dataType":"object"},
+ unauthorisedResponse: {"in":"res","name":"403","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ validationErrorResponse: {"in":"res","name":"400","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ internalServerErrorResponse: {"in":"res","name":"500","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"message":{"dataType":"string","required":true}}},
+ };
+ app.post('/api/auth/create-user',
+ ...(fetchMiddlewares(AuthController)),
+ ...(fetchMiddlewares(AuthController.prototype.createUser)),
+
+ async function AuthController_createUser(request: ExRequest, response: ExResponse, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ let validatedArgs: any[] = [];
+ try {
+ validatedArgs = templateService.getValidatedArgs({ args: argsAuthController_createUser, request, response });
+
+ const controller = new AuthController();
+
+ await templateService.apiHandler({
+ methodName: 'createUser',
+ controller,
+ response,
+ next,
+ validatedArgs,
+ successStatus: undefined,
+ });
+ } catch (err) {
+ return next(err);
+ }
+ });
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ function authenticateMiddleware(security: TsoaRoute.Security[] = []) {
+ return async function runAuthenticationMiddleware(request: any, response: any, next: any) {
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ // keep track of failed auth attempts so we can hand back the most
+ // recent one. This behavior was previously existing so preserving it
+ // here
+ const failedAttempts: any[] = [];
+ const pushAndRethrow = (error: any) => {
+ failedAttempts.push(error);
+ throw error;
+ };
+
+ const secMethodOrPromises: Promise[] = [];
+ for (const secMethod of security) {
+ if (Object.keys(secMethod).length > 1) {
+ const secMethodAndPromises: Promise[] = [];
+
+ for (const name in secMethod) {
+ secMethodAndPromises.push(
+ expressAuthenticationRecasted(request, name, secMethod[name], response)
+ .catch(pushAndRethrow)
+ );
+ }
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ secMethodOrPromises.push(Promise.all(secMethodAndPromises)
+ .then(users => { return users[0]; }));
+ } else {
+ for (const name in secMethod) {
+ secMethodOrPromises.push(
+ expressAuthenticationRecasted(request, name, secMethod[name], response)
+ .catch(pushAndRethrow)
+ );
+ }
+ }
+ }
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+
+ try {
+ request['user'] = await Promise.any(secMethodOrPromises);
+
+ // Response was sent in middleware, abort
+ if (response.writableEnded) {
+ return;
+ }
+
+ next();
+ }
+ catch(err) {
+ // Show most recent error as response
+ const error = failedAttempts.pop();
+ error.status = error.status || 401;
+
+ // Response was sent in middleware, abort
+ if (response.writableEnded) {
+ return;
+ }
+ next(error);
+ }
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+ }
+ }
+
+ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
+}
+
+// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
diff --git a/src/service/index.ts b/src/service/index.ts
index 831a1c0fb..0084c88a0 100644
--- a/src/service/index.ts
+++ b/src/service/index.ts
@@ -27,8 +27,11 @@ import lusca from 'lusca';
import * as config from '../config';
import * as db from '../db';
import { Proxy } from '../proxy';
-import routes from './routes';
+import { RegisterRoutes } from './generatedRoutes';
+import { setProxy } from './proxyStore';
import { configure } from './passport';
+import { ValidateError } from 'tsoa';
+import type { Request, Response, NextFunction } from 'express';
const limiter = rateLimit(config.getRateLimit());
@@ -174,7 +177,33 @@ async function createApp(proxy: Proxy): Promise {
app.use(passport.session());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
- app.use('/', routes(proxy));
+
+ // Make the Proxy instance available to controllers before registering routes.
+ if (proxy) setProxy(proxy);
+ RegisterRoutes(app);
+
+ // Error handlers — must be registered after routes.
+ // Handle tsoa validation errors (missing/invalid fields).
+ app.use((err: unknown, _req: Request, res: Response, next: NextFunction) => {
+ if (err instanceof ValidateError) {
+ return res.status(400).json({
+ message: 'Validation failed',
+ details: err.fields,
+ });
+ }
+ next(err);
+ });
+ // Handle HTTP errors thrown from controllers (objects with a .status property).
+ app.use((err: unknown, _req: Request, res: Response, next: NextFunction) => {
+ const httpErr = err as { status?: number; message?: string };
+ const status = typeof httpErr.status === 'number' ? httpErr.status : 500;
+ const message = httpErr.message ?? 'Internal server error';
+ if (!res.headersSent) {
+ return res.status(status).json({ message });
+ }
+ next(err);
+ });
+
app.use('/', express.static(absBuildPath));
app.get('/*path', (_req, res) => {
res.sendFile(path.join(`${absBuildPath}/index.html`));
diff --git a/src/service/interfaces/auth.interfaces.ts b/src/service/interfaces/auth.interfaces.ts
new file mode 100644
index 000000000..f1d1a79ef
--- /dev/null
+++ b/src/service/interfaces/auth.interfaces.ts
@@ -0,0 +1,61 @@
+/**
+ * Copyright 2026 GitProxy Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { PublicUser } from '../../db/types';
+
+export interface AuthResources {
+ login: { action: 'post'; uri: string };
+ profile: { action: 'get'; uri: string };
+ logout: { action: 'post'; uri: string };
+}
+
+export interface AuthConfigResponse {
+ usernamePasswordMethod: string | null;
+ otherMethods: string[];
+}
+
+export interface LoginResponse {
+ message: 'success';
+ user: PublicUser;
+}
+
+export interface LogoutResponse {
+ isAuth: boolean;
+ user: Express.User | undefined;
+}
+
+export interface CreateUserResponse {
+ message: string;
+ username: string;
+}
+
+export interface CsrfTokenResponse {
+ csrfToken: string;
+}
+
+export interface GitAccountBody {
+ username?: string;
+ id?: string;
+ gitAccount: string;
+}
+
+export interface CreateUserBody {
+ username: string;
+ password: string;
+ email: string;
+ gitAccount: string;
+ admin?: boolean;
+}
diff --git a/src/service/interfaces/common.interfaces.ts b/src/service/interfaces/common.interfaces.ts
new file mode 100644
index 000000000..1e3efbb32
--- /dev/null
+++ b/src/service/interfaces/common.interfaces.ts
@@ -0,0 +1,20 @@
+/**
+ * Copyright 2026 GitProxy Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/** Generic response carrying a human-readable message. */
+export interface MessageResponse {
+ message: string;
+}
diff --git a/src/service/interfaces/health.interfaces.ts b/src/service/interfaces/health.interfaces.ts
new file mode 100644
index 000000000..e105259fe
--- /dev/null
+++ b/src/service/interfaces/health.interfaces.ts
@@ -0,0 +1,19 @@
+/**
+ * Copyright 2026 GitProxy Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export interface HealthResponse {
+ message: 'ok';
+}
diff --git a/src/service/interfaces/home.interfaces.ts b/src/service/interfaces/home.interfaces.ts
new file mode 100644
index 000000000..5914707cf
--- /dev/null
+++ b/src/service/interfaces/home.interfaces.ts
@@ -0,0 +1,21 @@
+/**
+ * Copyright 2026 GitProxy Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export interface ApiResources {
+ healthcheck: string;
+ push: string;
+ auth: string;
+}
diff --git a/src/service/routes/home.ts b/src/service/interfaces/push.interfaces.ts
similarity index 67%
rename from src/service/routes/home.ts
rename to src/service/interfaces/push.interfaces.ts
index bfab99c46..94bb90344 100644
--- a/src/service/routes/home.ts
+++ b/src/service/interfaces/push.interfaces.ts
@@ -14,18 +14,15 @@
* limitations under the License.
*/
-import express, { Request, Response } from 'express';
+import { AttestationAnswer } from '../../proxy/processors/types';
-const router = express.Router();
+export interface RejectBody {
+ /** The reason for rejecting the push request. */
+ reason: string;
+}
-const resource = {
- healthcheck: '/api/v1/healthcheck',
- push: '/api/v1/push',
- auth: '/api/auth',
-};
-
-router.get('/', (_req: Request, res: Response) => {
- res.send(resource);
-});
-
-export default router;
+export interface AuthoriseBody {
+ params: {
+ attestation: AttestationAnswer[];
+ };
+}
diff --git a/src/service/interfaces/repo.interfaces.ts b/src/service/interfaces/repo.interfaces.ts
new file mode 100644
index 000000000..df2177a9d
--- /dev/null
+++ b/src/service/interfaces/repo.interfaces.ts
@@ -0,0 +1,33 @@
+/**
+ * Copyright 2026 GitProxy Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Repo } from '../../db/types';
+
+export interface UsernameBody {
+ username: string;
+}
+
+export interface CreateRepoBody {
+ url: string;
+ name: string;
+ project: string;
+ users?: { canPush: string[]; canAuthorise: string[] };
+ _id?: string;
+}
+
+export interface RepoWithProxy extends Repo {
+ proxyURL: string;
+}
diff --git a/src/service/interfaces/user.interfaces.ts b/src/service/interfaces/user.interfaces.ts
new file mode 100644
index 000000000..8df34469b
--- /dev/null
+++ b/src/service/interfaces/user.interfaces.ts
@@ -0,0 +1,37 @@
+/**
+ * Copyright 2026 GitProxy Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export interface SSHKeyFingerprint {
+ fingerprint: string;
+ name: string;
+ addedAt: string;
+}
+
+export interface AddSSHKeyBody {
+ // Optional so the controller performs its own validation and returns a custom
+ // `{ error: 'Public key is required' }` response rather than tsoa's ValidateError.
+ publicKey?: string;
+ name?: string;
+}
+
+export interface AddSSHKeyResponse {
+ message: string;
+ fingerprint: string;
+}
+
+export interface RemoveSSHKeyResponse {
+ message: string;
+}
diff --git a/src/service/passport/jwtAuthHandler.ts b/src/service/passport/jwtAuthHandler.ts
deleted file mode 100644
index d03fe5555..000000000
--- a/src/service/passport/jwtAuthHandler.ts
+++ /dev/null
@@ -1,101 +0,0 @@
-/**
- * Copyright 2026 GitProxy Contributors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import { assignRoles, validateJwt } from './jwtUtils';
-import type { Request, Response, NextFunction } from 'express';
-import { getAPIAuthMethods } from '../../config';
-import {
- AuthenticationElement,
- JwtConfig,
- RoleMapping,
- AuthenticationElementType,
-} from '../../config/generated/config';
-
-export const type = 'jwt';
-
-export const jwtAuthHandler = (overrideConfig: JwtConfig | null = null) => {
- return async (req: Request, res: Response, next: NextFunction): Promise => {
- const apiAuthMethods: AuthenticationElement[] = overrideConfig
- ? [{ type: 'jwt' as AuthenticationElementType, enabled: true, jwtConfig: overrideConfig }]
- : getAPIAuthMethods();
-
- const jwtAuthMethod = apiAuthMethods.find((method) => method.type.toLowerCase() === type);
-
- if (!jwtAuthMethod || !jwtAuthMethod.enabled) {
- return next();
- }
-
- if (req.isAuthenticated && req.isAuthenticated()) {
- return next();
- }
-
- const token = req.header('Authorization');
- if (!token) {
- res.status(401).send('No token provided\n');
- return;
- }
-
- if (!jwtAuthMethod.jwtConfig) {
- res.status(500).send({
- message: 'JWT configuration is missing\n',
- });
- console.log('JWT configuration is missing\n');
- return;
- }
-
- const config = jwtAuthMethod.jwtConfig!;
- const { clientID, authorityURL, expectedAudience, roleMapping } = config;
- const audience = expectedAudience || clientID;
-
- if (!authorityURL) {
- res.status(500).send({
- message: 'OIDC authority URL is not configured\n',
- });
- console.log('OIDC authority URL is not configured\n');
- return;
- }
-
- if (!clientID) {
- res.status(500).send({
- message: 'OIDC client ID is not configured\n',
- });
- console.log('OIDC client ID is not configured\n');
- return;
- }
-
- const tokenParts = token.split(' ');
- const accessToken = tokenParts.length === 2 ? tokenParts[1] : tokenParts[0];
-
- const { verifiedPayload, error } = await validateJwt(
- accessToken,
- authorityURL,
- audience,
- clientID,
- );
-
- if (error || !verifiedPayload) {
- res.status(401).send(error || 'JWT validation failed\n');
- console.log('JWT validation failed\n');
- return;
- }
-
- req.user = verifiedPayload;
- assignRoles(roleMapping as RoleMapping, verifiedPayload, req.user);
-
- console.log('JWT validation successful\n');
- next();
- };
-};
diff --git a/src/service/proxyStore.ts b/src/service/proxyStore.ts
new file mode 100644
index 000000000..f84704072
--- /dev/null
+++ b/src/service/proxyStore.ts
@@ -0,0 +1,34 @@
+/**
+ * Copyright 2026 GitProxy Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Proxy } from '../proxy';
+
+let _proxy: Proxy | null = null;
+
+/**
+ * Store the Proxy instance so controllers can access it without constructor injection.
+ * Must be called before RegisterRoutes is invoked in src/service/index.ts.
+ */
+export function setProxy(proxy: Proxy): void {
+ _proxy = proxy;
+}
+
+export function getProxy(): Proxy {
+ if (!_proxy) {
+ throw new Error('Proxy has not been initialised. Call setProxy() before using getProxy().');
+ }
+ return _proxy;
+}
diff --git a/src/service/routes/auth.ts b/src/service/routes/auth.ts
deleted file mode 100644
index adaca92ce..000000000
--- a/src/service/routes/auth.ts
+++ /dev/null
@@ -1,284 +0,0 @@
-/**
- * Copyright 2026 GitProxy Contributors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import express, { Request, Response, NextFunction } from 'express';
-import { getPassport, authStrategies } from '../passport';
-import { getAuthMethods, getUIHost, getUIPort } from '../../config';
-
-import * as db from '../../db';
-import * as passportLocal from '../passport/local';
-import * as passportAD from '../passport/activeDirectory';
-
-import { User } from '../../db/types';
-import { AuthenticationElement } from '../../config/generated/config';
-
-import { isAdminUser, toPublicUser } from './utils';
-import { handleErrorAndLog } from '../../utils/errors';
-
-const router = express.Router();
-const passport = getPassport();
-
-router.get('/', (_req: Request, res: Response) => {
- res.status(200).json({
- login: {
- action: 'post',
- uri: '/api/auth/login',
- },
- profile: {
- action: 'get',
- uri: '/api/auth/profile',
- },
- logout: {
- action: 'post',
- uri: '/api/auth/logout',
- },
- });
-});
-
-// login strategies that will work with /login e.g. take username and password
-const appropriateLoginStrategies = [passportLocal.type, passportAD.type];
-// getLoginStrategy fetches the enabled auth methods and identifies if there's an appropriate
-// auth method for username and password login. If there isn't it returns null, if there is it
-// returns the first.
-const getLoginStrategy = () => {
- // returns only enabled auth methods
- // returns at least one enabled auth method
- const enabledAppropriateLoginStrategies = getAuthMethods().filter((am: AuthenticationElement) =>
- appropriateLoginStrategies.includes(am.type.toLowerCase()),
- );
- // for where no login strategies which work for /login are enabled
- // just return null
- if (enabledAppropriateLoginStrategies.length === 0) {
- return null;
- }
- // return the first enabled auth method
- return enabledAppropriateLoginStrategies[0].type.toLowerCase();
-};
-
-const loginSuccessHandler = () => async (req: Request, res: Response) => {
- try {
- const currentUser = toPublicUser({ ...req.user } as User);
- console.log(
- `serivce.routes.auth.login: user logged in, username=${
- currentUser.username
- } profile=${JSON.stringify(currentUser)}`,
- );
- res.send({
- message: 'success',
- user: currentUser,
- });
- } catch (error: unknown) {
- const msg = handleErrorAndLog(error, 'Error logging user in');
- res.status(500).send(`Failed to login: ${msg}`).end();
- }
-};
-
-router.get('/config', (req, res) => {
- const usernamePasswordMethod = getLoginStrategy();
- res.send({
- // enabled username /password auth method
- usernamePasswordMethod: usernamePasswordMethod,
- // other enabled auth methods
- otherMethods: getAuthMethods()
- .map((am) => am.type.toLowerCase())
- .filter((authType) => authType !== usernamePasswordMethod),
- });
-});
-
-// TODO: provide separate auth endpoints for each auth strategy or chain compatibile auth strategies
-// TODO: if providing separate auth methods, inform the frontend so it has relevant UI elements and appropriate client-side behavior
-router.post(
- '/login',
- (req: Request, res: Response, next: NextFunction) => {
- const authType = getLoginStrategy();
- if (authType === null) {
- res.status(403).send('Username and Password based Login is not enabled at this time').end();
- return;
- }
- console.log('going to auth with', authType);
- return passport.authenticate(authType)(req, res, next);
- },
- loginSuccessHandler(),
-);
-
-router.get('/openidconnect', passport.authenticate(authStrategies['openidconnect'].type));
-
-router.get('/openidconnect/callback', (req: Request, res: Response, next: NextFunction) => {
- passport.authenticate(
- authStrategies['openidconnect'].type,
- (err: unknown, user: Partial, info: unknown) => {
- if (err) {
- console.error('Authentication error:', err);
- return res.status(500).end();
- }
- if (!user) {
- console.error('No user found:', info);
- return res.status(401).end();
- }
- req.logIn(user, (err) => {
- if (err) {
- console.error('Login error:', err);
- return res.status(500).end();
- }
- console.log('Logged in successfully. User:', user);
- return res.redirect(`${getUIHost()}:${getUIPort()}/dashboard/profile`);
- });
- },
- )(req, res, next);
-});
-
-router.post('/logout', (req: Request, res: Response, next: NextFunction) => {
- req.logout((err: unknown) => {
- if (err) return next(err);
- });
- res.clearCookie('connect.sid');
- res.send({ isAuth: req.isAuthenticated(), user: req.user });
-});
-
-router.get('/profile', async (req: Request, res: Response) => {
- if (!req.user) {
- res
- .status(401)
- .send({
- message: 'Not logged in',
- })
- .end();
- return;
- }
-
- const userVal = await db.findUser((req.user as User).username);
- if (!userVal) {
- res.status(404).send({ message: 'User not found' }).end();
- return;
- }
-
- res.send(toPublicUser(userVal));
-});
-
-router.post('/gitAccount', async (req: Request, res: Response) => {
- if (!req.user) {
- res
- .status(401)
- .send({
- message: 'Not logged in',
- })
- .end();
- return;
- }
-
- try {
- let username =
- req.body.username == null || req.body.username === 'undefined'
- ? req.body.id
- : req.body.username;
- username = username?.split('@')[0];
-
- if (!username) {
- res
- .status(400)
- .send({
- message: 'Missing username. Git account not updated',
- })
- .end();
- return;
- }
-
- const reqUser = await db.findUser((req.user as User).username);
- if (username !== reqUser?.username && !reqUser?.admin) {
- res
- .status(403)
- .send({
- message: 'Must be an admin to update a different account',
- })
- .end();
- return;
- }
-
- const user = await db.findUser(username);
- if (!user) {
- res
- .status(404)
- .send({
- message: 'User not found',
- })
- .end();
- return;
- }
-
- user.gitAccount = req.body.gitAccount;
- db.updateUser(user);
- res.status(200).end();
- } catch (error: unknown) {
- const msg = handleErrorAndLog(error, 'Failed to update git account');
- res
- .status(500)
- .send({
- message: msg,
- })
- .end();
- }
-});
-
-router.post('/create-user', async (req: Request, res: Response) => {
- if (!isAdminUser(req.user)) {
- res
- .status(403)
- .send({
- message: 'Not authorized to create users',
- })
- .end();
- return;
- }
-
- try {
- const { username, password, email, gitAccount, admin: isAdmin = false } = req.body;
-
- if (!username || !password || !email || !gitAccount) {
- res
- .status(400)
- .send({
- message:
- 'Missing required fields: username, password, email, and gitAccount are required',
- })
- .end();
- return;
- }
-
- await db.createUser(username, password, email, gitAccount, isAdmin);
- res
- .status(201)
- .send({
- message: 'User created successfully',
- username,
- })
- .end();
- } catch (error: unknown) {
- const msg = handleErrorAndLog(error, 'Failed to create user');
- res
- .status(500)
- .send({
- message: msg,
- })
- .end();
- }
-});
-
-router.get('/csrf-token', (req: Request, res: Response) => {
- console.log('req.user', req.user);
- res.send({ csrfToken: (req as any).csrfToken() });
-});
-
-export default { router, loginSuccessHandler };
diff --git a/src/service/routes/config.ts b/src/service/routes/config.ts
deleted file mode 100644
index d7b3c66d2..000000000
--- a/src/service/routes/config.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-/**
- * Copyright 2026 GitProxy Contributors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import express, { Request, Response } from 'express';
-import * as config from '../../config';
-
-const router = express.Router();
-
-router.get('/attestation', (_req: Request, res: Response) => {
- res.send(config.getAttestationConfig());
-});
-
-router.get('/urlShortener', (_req: Request, res: Response) => {
- res.send(config.getURLShortener());
-});
-
-router.get('/contactEmail', (_req: Request, res: Response) => {
- res.send(config.getContactEmail());
-});
-
-router.get('/uiRouteAuth', (_req: Request, res: Response) => {
- res.send(config.getUIRouteAuth());
-});
-
-router.get('/ssh', (_req: Request, res: Response) => {
- res.send(config.getSSHConfig());
-});
-
-export default router;
diff --git a/src/service/routes/index.ts b/src/service/routes/index.ts
deleted file mode 100644
index 80d6c315d..000000000
--- a/src/service/routes/index.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-/**
- * Copyright 2026 GitProxy Contributors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import express from 'express';
-import auth from './auth';
-import push from './push';
-import home from './home';
-import repo from './repo';
-import users from './users';
-import healthcheck from './healthcheck';
-import config from './config';
-import { jwtAuthHandler } from '../passport/jwtAuthHandler';
-import { Proxy } from '../../proxy';
-
-const routes = (proxy: Proxy) => {
- const router = express.Router();
- router.use('/api', home);
- router.use('/api/auth', auth.router);
- router.use('/api/v1/healthcheck', healthcheck);
- router.use('/api/v1/push', jwtAuthHandler(), push);
- router.use('/api/v1/repo', jwtAuthHandler(), repo(proxy));
- router.use('/api/v1/user', jwtAuthHandler(), users);
- router.use('/api/v1/config', config);
- return router;
-};
-
-export default routes;
diff --git a/src/service/routes/push.ts b/src/service/routes/push.ts
deleted file mode 100644
index 1e900d6ea..000000000
--- a/src/service/routes/push.ts
+++ /dev/null
@@ -1,278 +0,0 @@
-/**
- * Copyright 2026 GitProxy Contributors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import express, { Request, Response } from 'express';
-import * as db from '../../db';
-import { PushQuery } from '../../db/types';
-import { AttestationConfig } from '../../config/generated/config';
-import { getAttestationConfig } from '../../config';
-import { AttestationAnswer, Rejection } from '../../proxy/processors/types';
-
-interface AuthoriseRequest {
- params: {
- attestation: AttestationAnswer[];
- };
-}
-
-const router = express.Router();
-
-router.get('/', async (req: Request, res: Response) => {
- const query: Partial = {
- type: 'push',
- };
-
- for (const key in req.query) {
- if (!key) continue;
- if (key === 'limit' || key === 'skip') continue;
-
- const rawValue = req.query[key];
- let parsedValue: boolean | undefined;
- if (rawValue === 'false') parsedValue = false;
- if (rawValue === 'true') parsedValue = true;
- query[key] = parsedValue ?? rawValue?.toString();
- }
-
- res.send(await db.getPushes(query));
-});
-
-router.get('/:id', async (req: Request<{ id: string }>, res: Response) => {
- const id = req.params.id;
- const push = await db.getPush(id);
- if (push) {
- res.send(push);
- } else {
- res.status(404).send({
- message: 'not found',
- });
- }
-});
-
-router.post('/:id/reject', async (req: Request<{ id: string }>, res: Response) => {
- if (!req.user) {
- res.status(401).send({
- message: 'Not logged in',
- });
- return;
- }
-
- const id = req.params.id;
- const { username } = req.user as { username: string };
- const { reason } = req.body;
-
- if (!reason || !reason.trim()) {
- res.status(400).send({
- message: 'Rejection reason is required',
- });
- return;
- }
-
- // Get the push request
- const push = await getValidPushOrRespond(id, res);
- if (!push) return;
-
- // Get the committer of the push via their email
- const committerEmail = push.userEmail;
- const list = await db.getUsers({ email: committerEmail });
-
- if (list.length === 0) {
- res.status(404).send({
- message: `No user found with the committer's email address: ${committerEmail}`,
- });
- return;
- }
-
- if (list[0].username.toLowerCase() === username.toLowerCase() && !list[0].admin) {
- res.status(403).send({
- message: `Cannot reject your own changes`,
- });
- return;
- }
-
- const isAllowed = await db.canUserApproveRejectPush(id, username);
-
- if (isAllowed) {
- const reviewerList = await db.getUsers({ username });
- const reviewerEmail = reviewerList[0].email;
-
- if (!reviewerEmail) {
- res.status(404).send({
- message: `There was no registered email address for the reviewer: ${username}`,
- });
- return;
- }
-
- const rejection: Rejection = {
- reason,
- timestamp: new Date(),
- reviewer: {
- username,
- email: reviewerEmail,
- },
- };
-
- const result = await db.reject(id, rejection);
- console.log(
- `User ${username} rejected push request for ${id}${reason ? ` with reason: ${reason}` : ''}`,
- );
- res.send(result);
- } else {
- res.status(403).send({
- message: `User ${username} is not authorised to reject changes on this project`,
- });
- }
-});
-
-router.post(
- '/:id/authorise',
- async (req: Request<{ id: string }, unknown, AuthoriseRequest>, res: Response) => {
- if (!req.user) {
- res.status(401).send({
- message: 'Not logged in',
- });
- return;
- }
-
- const answers = req.body.params?.attestation;
-
- const attestationComplete = validateAttestation(answers, getAttestationConfig());
-
- if (!attestationComplete) {
- res.status(400).send({
- message: 'Attestation is not complete',
- });
- return;
- }
-
- const id = req.params.id;
-
- const { username } = req.user as { username: string };
-
- const push = await db.getPush(id);
- if (!push) {
- res.status(404).send({
- message: 'Push request not found',
- });
- return;
- }
-
- // Get the committer of the push via their email address
- const committerEmail = push.userEmail;
-
- const list = await db.getUsers({ email: committerEmail });
-
- if (list.length === 0) {
- res.status(404).send({
- message: `No user found with the committer's email address: ${committerEmail}`,
- });
- return;
- }
-
- if (list[0].username.toLowerCase() === username.toLowerCase() && !list[0].admin) {
- res.status(403).send({
- message: `Cannot approve your own changes`,
- });
- return;
- }
-
- // If we are not the author, now check that we are allowed to authorise on this
- // repo
- const isAllowed = await db.canUserApproveRejectPush(id, username);
- if (isAllowed) {
- console.log(`User ${username} approved push request for ${id}`);
-
- const reviewerList = await db.getUsers({ username });
- const reviewerEmail = reviewerList[0].email;
-
- if (!reviewerEmail) {
- res.status(404).send({
- message: `There was no registered email address for the reviewer: ${username}`,
- });
- return;
- }
-
- const attestation = {
- answers,
- timestamp: new Date(),
- reviewer: {
- username,
- email: reviewerEmail,
- },
- };
- const result = await db.authorise(id, attestation);
- res.send(result);
- } else {
- res.status(403).send({
- message: `User ${username} not authorised to approve pushes on this project`,
- });
- }
- },
-);
-
-router.post('/:id/cancel', async (req: Request<{ id: string }>, res: Response) => {
- if (!req.user) {
- res.status(401).send({
- message: 'Not logged in',
- });
- return;
- }
-
- const id = req.params.id;
- const { username } = req.user as { username: string };
-
- const isAllowed = await db.canUserCancelPush(id, username);
-
- if (isAllowed) {
- const result = await db.cancel(id);
- console.log(`User ${username} canceled push request for ${id}`);
- res.send(result);
- } else {
- console.log(`User ${username} not authorised to cancel push request for ${id}`);
- res.status(403).send({
- message: `User ${username} not authorised to cancel push requests on this project`,
- });
- }
-});
-
-async function getValidPushOrRespond(id: string, res: Response) {
- console.log('getValidPushOrRespond', { id });
- const push = await db.getPush(id);
-
- if (!push) {
- res.status(404).send({ message: `Push request not found` });
- return null;
- }
-
- if (!push.userEmail) {
- res.status(400).send({ message: `Push request has no user email` });
- return null;
- }
-
- return push;
-}
-
-function validateAttestation(answers: AttestationAnswer[], config: AttestationConfig): boolean {
- const configQuestions = config.questions ?? [];
-
- if (answers.length !== configQuestions.length) {
- return false;
- }
-
- const configLabels = new Set(configQuestions.map((q) => q.label));
-
- return answers.every((answer) => configLabels.has(answer.label) && !!answer.checked);
-}
-
-export default router;
diff --git a/src/service/routes/repo.ts b/src/service/routes/repo.ts
deleted file mode 100644
index 7e259d0aa..000000000
--- a/src/service/routes/repo.ts
+++ /dev/null
@@ -1,231 +0,0 @@
-/**
- * Copyright 2026 GitProxy Contributors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import express, { Request, Response } from 'express';
-
-import * as db from '../../db';
-import { getProxyURL } from '../urls';
-import { getAllProxiedHosts } from '../../db';
-import { RepoQuery } from '../../db/types';
-import { isAdminUser } from './utils';
-import { Proxy } from '../../proxy';
-import { handleErrorAndLog } from '../../utils/errors';
-
-function repo(proxy: Proxy) {
- const router = express.Router();
-
- router.get('/', async (req: Request, res: Response) => {
- const proxyURL = getProxyURL(req);
- const query: Partial = {};
-
- for (const key in req.query) {
- if (!key) continue;
- if (key === 'limit' || key === 'skip') continue;
-
- const rawValue = req.query[key];
- let parsedValue: boolean | undefined;
- if (rawValue === 'false') parsedValue = false;
- if (rawValue === 'true') parsedValue = true;
- query[key] = parsedValue ?? rawValue?.toString();
- }
-
- const qd = await db.getRepos(query);
- res.send(qd.map((d) => ({ ...d, proxyURL })));
- });
-
- router.get('/:id', async (req: Request<{ id: string }>, res: Response) => {
- const proxyURL = getProxyURL(req);
- const _id = req.params.id;
- const qd = await db.getRepoById(_id);
- res.send({ ...qd, proxyURL });
- });
-
- router.patch('/:id/user/push', async (req: Request<{ id: string }>, res: Response) => {
- if (!isAdminUser(req.user)) {
- res.status(401).send({
- message: 'You are not authorised to perform this action.',
- });
- return;
- }
-
- const _id = req.params.id;
- const username = req.body.username.toLowerCase();
- const user = await db.findUser(username);
-
- if (!user) {
- res.status(400).send({ error: 'User does not exist' });
- return;
- }
-
- await db.addUserCanPush(_id, username);
- res.send({ message: 'created' });
- });
-
- router.patch('/:id/user/authorise', async (req: Request<{ id: string }>, res: Response) => {
- if (!isAdminUser(req.user)) {
- res.status(401).send({
- message: 'You are not authorised to perform this action.',
- });
- return;
- }
-
- const _id = req.params.id;
- const username = req.body.username;
- const user = await db.findUser(username);
-
- if (!user) {
- res.status(400).send({ error: 'User does not exist' });
- return;
- }
-
- await db.addUserCanAuthorise(_id, username);
- res.send({ message: 'created' });
- });
-
- router.delete(
- '/:id/user/authorise/:username',
- async (req: Request<{ id: string; username: string }>, res: Response) => {
- if (!isAdminUser(req.user)) {
- res.status(401).send({
- message: 'You are not authorised to perform this action.',
- });
- return;
- }
-
- const _id = req.params.id;
- const username = req.params.username;
- const user = await db.findUser(username);
-
- if (!user) {
- res.status(400).send({ error: 'User does not exist' });
- return;
- }
-
- await db.removeUserCanAuthorise(_id, username);
- res.send({ message: 'created' });
- },
- );
-
- router.delete(
- '/:id/user/push/:username',
- async (req: Request<{ id: string; username: string }>, res: Response) => {
- if (!isAdminUser(req.user)) {
- res.status(401).send({
- message: 'You are not authorised to perform this action.',
- });
- return;
- }
-
- const _id = req.params.id;
- const username = req.params.username;
- const user = await db.findUser(username);
-
- if (!user) {
- res.status(400).send({ error: 'User does not exist' });
- return;
- }
-
- await db.removeUserCanPush(_id, username);
- res.send({ message: 'created' });
- },
- );
-
- router.delete('/:id/delete', async (req: Request<{ id: string }>, res: Response) => {
- if (!isAdminUser(req.user)) {
- res.status(401).send({
- message: 'You are not authorised to perform this action.',
- });
- return;
- }
-
- const _id = req.params.id;
-
- // determine if we need to restart the proxy
- const previousHosts = await getAllProxiedHosts();
- await db.deleteRepo(_id);
- const currentHosts = await getAllProxiedHosts();
-
- if (currentHosts.length < previousHosts.length) {
- // restart the proxy
- console.log('Restarting the proxy to remove a host');
- await proxy.stop();
- await proxy.start();
- }
-
- res.send({ message: 'deleted' });
- });
-
- router.post('/', async (req: Request, res: Response) => {
- if (!isAdminUser(req.user)) {
- res.status(401).send({
- message: 'You are not authorised to perform this action.',
- });
- return;
- }
-
- if (!req.body.url) {
- res.status(400).send({
- message: 'Repository url is required',
- });
- return;
- }
-
- const repo = await db.getRepoByUrl(req.body.url);
- if (repo) {
- res.status(409).send({
- message: `Repository ${req.body.url} already exists!`,
- });
- } else {
- try {
- // figure out if this represent a new domain to proxy
- let newOrigin = true;
-
- const existingHosts = await getAllProxiedHosts();
- existingHosts.forEach((h) => {
- // assume SSL is in use and that our origins are missing the protocol
- if (req.body.url.startsWith(`https://${h}`)) {
- newOrigin = false;
- }
- });
-
- console.log(
- `API request to proxy repository ${req.body.url} is for a new origin: ${newOrigin},\n\texisting origin list was: ${JSON.stringify(existingHosts)}`,
- );
-
- // create the repository
- const repoDetails = await db.createRepo(req.body);
- const proxyURL = getProxyURL(req);
-
- // restart the proxy if we're proxying a new domain
- if (newOrigin) {
- console.log('Restarting the proxy to handle an additional host');
- await proxy.stop();
- await proxy.start();
- }
-
- // return data on the new repository (including it's _id and the proxyUrl)
- res.send({ ...repoDetails, proxyURL, message: 'created' });
- } catch (error: unknown) {
- const msg = handleErrorAndLog(error, 'Repository creation failed');
- res.status(500).send({ message: msg });
- }
- }
- });
-
- return router;
-}
-
-export default repo;
diff --git a/src/service/routes/users.ts b/src/service/routes/users.ts
deleted file mode 100644
index 385ad1da0..000000000
--- a/src/service/routes/users.ts
+++ /dev/null
@@ -1,194 +0,0 @@
-/**
- * Copyright 2026 GitProxy Contributors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import express, { Request, Response } from 'express';
-import crypto from 'crypto';
-
-import * as db from '../../db';
-import { toPublicUser } from './utils';
-
-const router = express.Router();
-
-// Calculate SHA-256 fingerprint from SSH public key
-// Note: This function is duplicated in src/cli/ssh-key.ts to keep CLI and server independent
-function calculateFingerprint(publicKeyStr: string): string | null {
- try {
- const { utils } = require('ssh2');
- const parsed = utils.parseKey(publicKeyStr);
- if (!parsed || parsed instanceof Error) {
- return null;
- }
- const pubKey = parsed.getPublicSSH();
- const hash = crypto.createHash('sha256').update(pubKey).digest('base64');
- return `SHA256:${hash}`;
- } catch (err) {
- console.error('Error calculating fingerprint:', err);
- return null;
- }
-}
-
-router.get('/', async (req: Request, res: Response) => {
- console.log('fetching users');
- const users = await db.getUsers();
- res.send(users.map(toPublicUser));
-});
-
-router.get('/:id', async (req: Request<{ id: string }>, res: Response) => {
- const username = req.params.id.toLowerCase();
- console.log(`Retrieving details for user: ${username}`);
- const user = await db.findUser(username);
- if (!user) {
- res
- .status(404)
- .send({
- message: `User ${username} not found`,
- })
- .end();
- return;
- }
- res.send(toPublicUser(user));
-});
-
-// Get SSH key fingerprints for a user
-router.get(
- '/:username/ssh-key-fingerprints',
- async (req: Request<{ username: string }>, res: Response) => {
- if (!req.user) {
- res.status(401).json({ error: 'Authentication required' });
- return;
- }
-
- const { username, admin } = req.user as { username: string; admin: boolean };
- const targetUsername = req.params.username.toLowerCase();
-
- // Only allow users to view their own keys, or admins to view any keys
- if (username !== targetUsername && !admin) {
- res.status(403).json({ error: 'Not authorized to view keys for this user' });
- return;
- }
-
- try {
- const publicKeys = await db.getPublicKeys(targetUsername);
- const keyFingerprints = publicKeys.map((keyRecord) => ({
- fingerprint: keyRecord.fingerprint,
- name: keyRecord.name,
- addedAt: keyRecord.addedAt,
- }));
- res.json(keyFingerprints);
- } catch (error) {
- console.error('Error retrieving SSH keys:', error);
- res.status(500).json({ error: 'Failed to retrieve SSH keys' });
- }
- },
-);
-
-// Add SSH public key
-router.post('/:username/ssh-keys', async (req: Request<{ username: string }>, res: Response) => {
- if (!req.user) {
- res.status(401).json({ error: 'Authentication required' });
- return;
- }
-
- const { username, admin } = req.user as { username: string; admin: boolean };
- const targetUsername = req.params.username.toLowerCase();
-
- // Only allow users to add keys to their own account, or admins to add to any account
- if (username !== targetUsername && !admin) {
- res.status(403).json({ error: 'Not authorized to add keys for this user' });
- return;
- }
-
- const { publicKey, name } = req.body;
- if (!publicKey) {
- res.status(400).json({ error: 'Public key is required' });
- return;
- }
-
- // Strip the comment from the key (everything after the last space)
- const keyWithoutComment = publicKey.trim().split(' ').slice(0, 2).join(' ');
-
- // Calculate fingerprint
- const fingerprint = calculateFingerprint(keyWithoutComment);
- if (!fingerprint) {
- res.status(400).json({ error: 'Invalid SSH public key format' });
- return;
- }
-
- const publicKeyRecord = {
- key: keyWithoutComment,
- name: name || 'Unnamed Key',
- addedAt: new Date().toISOString(),
- fingerprint: fingerprint,
- };
-
- console.log('Adding SSH key', { targetUsername, fingerprint });
- try {
- await db.addPublicKey(targetUsername, publicKeyRecord);
- res.status(201).json({
- message: 'SSH key added successfully',
- fingerprint: fingerprint,
- });
- } catch (error: any) {
- console.error('Error adding SSH key:', error);
-
- // Return specific error message
- if (error.message === 'SSH key already exists') {
- res.status(409).json({ error: 'This SSH key already exists' });
- } else if (error.message === 'User not found') {
- res.status(404).json({ error: 'User not found' });
- } else {
- res.status(500).json({ error: error.message || 'Failed to add SSH key' });
- }
- }
-});
-
-// Remove SSH public key by fingerprint
-router.delete(
- '/:username/ssh-keys/:fingerprint',
- async (req: Request<{ username: string; fingerprint: string }>, res: Response) => {
- if (!req.user) {
- res.status(401).json({ error: 'Authentication required' });
- return;
- }
-
- const { username, admin } = req.user as { username: string; admin: boolean };
- const targetUsername = req.params.username.toLowerCase();
- const fingerprint = req.params.fingerprint;
-
- // Only allow users to remove keys from their own account, or admins to remove from any account
- if (username !== targetUsername && !admin) {
- res.status(403).json({ error: 'Not authorized to remove keys for this user' });
- return;
- }
-
- console.log('Removing SSH key', { targetUsername, fingerprint });
- try {
- await db.removePublicKey(targetUsername, fingerprint);
- res.status(200).json({ message: 'SSH key removed successfully' });
- } catch (error: any) {
- console.error('Error removing SSH key:', error);
-
- // Return specific error message
- if (error.message === 'User not found') {
- res.status(404).json({ error: 'User not found' });
- } else {
- res.status(500).json({ error: error.message || 'Failed to remove SSH key' });
- }
- }
- },
-);
-
-export default router;
diff --git a/src/ui/services/repo.ts b/src/ui/services/repo.ts
index 909127c98..690df885b 100644
--- a/src/ui/services/repo.ts
+++ b/src/ui/services/repo.ts
@@ -76,7 +76,7 @@ const getRepo = async (id: string): Promise> => {
}
};
-const addRepo = async (repo: RepoView): Promise> => {
+const addRepo = async (repo: Repo): Promise> => {
const apiV1Base = await getApiV1BaseUrl();
const url = new URL(`${apiV1Base}/repo`);
diff --git a/src/ui/views/RepoList/Components/NewRepo.tsx b/src/ui/views/RepoList/Components/NewRepo.tsx
index 11aa714a6..ee1d9327d 100644
--- a/src/ui/views/RepoList/Components/NewRepo.tsx
+++ b/src/ui/views/RepoList/Components/NewRepo.tsx
@@ -31,16 +31,16 @@ import { addRepo } from '../../../services/repo';
import { makeStyles } from '@material-ui/core/styles';
import styles from '../../../assets/jss/material-dashboard-react/views/dashboardStyle';
import { RepoIcon } from '@primer/octicons-react';
-import { RepoView } from '../../../types';
+import { Repo } from '../../../../db/types';
interface AddRepositoryDialogProps {
open: boolean;
onClose: () => void;
- onSuccess: (repo: RepoView) => void;
+ onSuccess: (repo: Repo) => void;
}
interface NewRepoProps {
- onSuccess: (repo: RepoView) => Promise;
+ onSuccess: (repo: Repo) => Promise;
}
const useStyles = makeStyles(styles as any);
@@ -59,7 +59,7 @@ const AddRepositoryDialog: React.FC = ({ open, onClose
onClose();
};
- const handleSuccess = (repo: RepoView) => {
+ const handleSuccess = (repo: Repo) => {
onSuccess(repo);
setTip(true);
};
@@ -71,11 +71,10 @@ const AddRepositoryDialog: React.FC = ({ open, onClose
};
const add = async () => {
- const repo: RepoView = {
+ const repo: Repo = {
project: project.trim(),
name: name.trim(),
url: url.trim(),
- proxyURL: '',
users: { canPush: [], canAuthorise: [] },
};
diff --git a/test/fixtures/test-package/package-lock.json b/test/fixtures/test-package/package-lock.json
index cc9cabe8f..121563add 100644
--- a/test/fixtures/test-package/package-lock.json
+++ b/test/fixtures/test-package/package-lock.json
@@ -13,36 +13,40 @@
},
"../../..": {
"name": "@finos/git-proxy",
- "version": "2.0.0-rc.3",
+ "version": "2.0.0",
"license": "Apache-2.0",
"workspaces": [
"./packages/git-proxy-cli"
],
"dependencies": {
- "@aws-sdk/credential-providers": "^3.940.0",
+ "@aws-sdk/credential-providers": "^3.980.0",
+ "@fontsource/roboto": "^5.2.9",
"@material-ui/core": "^4.12.4",
"@material-ui/icons": "4.11.3",
- "@primer/octicons-react": "^19.21.0",
+ "@primer/octicons-react": "^19.21.2",
"@seald-io/nedb": "^4.1.2",
- "axios": "^1.13.2",
+ "axios": "^1.13.4",
"bcryptjs": "^3.0.3",
"clsx": "^2.1.1",
"concurrently": "^9.2.1",
"connect-mongo": "^5.1.0",
- "cors": "^2.8.5",
- "diff2html": "^3.4.52",
- "env-paths": "^3.0.0",
+ "cors": "^2.8.6",
+ "diff2html": "^3.4.56",
+ "env-paths": "^4.0.0",
"escape-string-regexp": "^5.0.0",
- "express": "^5.1.0",
+ "express": "^5.2.1",
"express-http-proxy": "^2.1.2",
"express-rate-limit": "^8.2.1",
- "express-session": "^1.18.2",
+ "express-session": "^1.19.0",
+ "font-awesome": "^4.7.0",
"history": "5.3.0",
- "isomorphic-git": "^1.35.0",
- "jsonwebtoken": "^9.0.2",
+ "https-proxy-agent": "^7.0.6",
+ "isomorphic-git": "^1.36.3",
+ "jsonwebtoken": "^9.0.3",
"load-plugin": "^6.0.3",
- "lodash": "^4.17.21",
+ "lodash": "^4.17.23",
"lusca": "^1.7.0",
+ "material-design-icons": "^3.0.1",
"moment": "^2.30.1",
"mongodb": "^5.9.2",
"openid-client": "^6.8.1",
@@ -51,15 +55,15 @@
"passport-activedirectory": "^1.4.0",
"passport-local": "^1.0.0",
"perfect-scrollbar": "^1.5.6",
- "prop-types": "15.8.1",
"react": "^16.14.0",
"react-dom": "^16.14.0",
"react-html-parser": "^2.0.2",
- "react-router-dom": "6.30.2",
+ "react-router-dom": "6.30.3",
"simple-git": "^3.30.0",
- "ssh2": "^1.17.0",
- "uuid": "^11.1.0",
- "validator": "^13.15.23",
+ "ssh2": "~1.17.0",
+ "tsoa": "^7.0.0-alpha.0",
+ "uuid": "^13.0.0",
+ "validator": "^13.15.26",
"yargs": "^17.7.2"
},
"bin": {
@@ -67,62 +71,65 @@
"git-proxy-all": "concurrently 'npm run server' 'npm run client'"
},
"devDependencies": {
- "@babel/core": "^7.28.5",
+ "@babel/core": "^7.28.6",
"@babel/preset-react": "^7.28.5",
"@commitlint/cli": "^19.8.1",
"@commitlint/config-conventional": "^19.8.1",
- "@eslint/compat": "^2.0.0",
- "@eslint/js": "^9.39.1",
- "@eslint/json": "^0.14.0",
+ "@eslint/compat": "^2.0.2",
+ "@eslint/js": "^9.39.2",
+ "@eslint/json": "^1.0.1",
"@types/activedirectory2": "^1.2.6",
"@types/cors": "^2.8.19",
"@types/domutils": "^2.1.0",
- "@types/express": "^5.0.5",
+ "@types/express": "^5.0.6",
"@types/express-http-proxy": "^1.6.7",
"@types/express-session": "^1.18.2",
"@types/jsonwebtoken": "^9.0.10",
- "@types/lodash": "^4.17.20",
+ "@types/lodash": "^4.17.23",
"@types/lusca": "^1.7.5",
- "@types/node": "^22.19.1",
+ "@types/node": "^22.19.7",
"@types/passport": "^1.0.17",
"@types/passport-local": "^1.0.38",
"@types/react-dom": "^17.0.26",
"@types/react-html-parser": "^2.0.7",
"@types/ssh2": "^1.15.5",
"@types/supertest": "^6.0.3",
- "@types/validator": "^13.15.9",
+ "@types/validator": "^13.15.10",
"@types/yargs": "^17.0.35",
- "@vitejs/plugin-react": "^5.1.1",
+ "@vitejs/plugin-react": "^5.1.2",
"@vitest/coverage-v8": "^3.2.4",
- "cypress": "^15.6.0",
- "eslint": "^9.39.1",
+ "c8": "^11.0.0",
+ "cross-env": "^10.1.0",
+ "cypress": "^15.9.0",
+ "eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
- "eslint-plugin-cypress": "^5.2.0",
+ "eslint-plugin-cypress": "^5.2.1",
+ "eslint-plugin-license-header": "^0.9.0",
"eslint-plugin-react": "^7.37.5",
- "fast-check": "^4.3.0",
- "globals": "^16.5.0",
+ "fast-check": "^4.5.3",
+ "globals": "^17.6.0",
"husky": "^9.1.7",
- "lint-staged": "^16.2.6",
+ "lint-staged": "^17.0.5",
"nyc": "^17.1.0",
- "prettier": "^3.6.2",
+ "prettier": "^3.8.1",
"quicktype": "^23.2.6",
- "supertest": "^7.1.4",
+ "supertest": "^7.2.2",
"ts-node": "^10.9.2",
- "tsx": "^4.20.6",
+ "tsx": "^4.21.0",
"typescript": "^5.9.3",
- "typescript-eslint": "^8.46.4",
- "vite": "^7.1.9",
+ "typescript-eslint": "^8.54.0",
+ "vite": "^7.3.1",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.2.4"
},
"engines": {
- "node": ">=20.19.2"
+ "node": ">=22.13.1 || >=24.0.0"
},
"optionalDependencies": {
- "@esbuild/darwin-arm64": "^0.27.0",
- "@esbuild/darwin-x64": "^0.27.0",
- "@esbuild/linux-x64": "0.27.0",
- "@esbuild/win32-x64": "0.27.0"
+ "@esbuild/darwin-arm64": "^0.27.2",
+ "@esbuild/darwin-x64": "^0.27.2",
+ "@esbuild/linux-x64": "0.27.2",
+ "@esbuild/win32-x64": "0.27.2"
}
},
"node_modules/@finos/git-proxy": {
diff --git a/test/service.tls.test.ts b/test/service.tls.test.ts
index dd0e163f9..5ef8bff43 100644
--- a/test/service.tls.test.ts
+++ b/test/service.tls.test.ts
@@ -87,6 +87,14 @@ describe('Service Module TLS', () => {
initialize: vi.fn().mockReturnValue((_req: any, _res: any, next: any) => next()),
session: vi.fn().mockReturnValue((_req: any, _res: any, next: any) => next()),
}),
+ getPassport: vi.fn().mockReturnValue({
+ authenticate: vi.fn().mockReturnValue((_req: any, _res: any, next: any) => next()),
+ }),
+ authStrategies: {
+ local: { type: 'local' },
+ activedirectory: { type: 'activedirectory' },
+ openidconnect: { type: 'openidconnect' },
+ },
}));
vi.doMock('../src/service/routes', () => ({
diff --git a/test/services/routes/auth.test.ts b/test/services/routes/auth.test.ts
index 41189edc0..6e3256881 100644
--- a/test/services/routes/auth.test.ts
+++ b/test/services/routes/auth.test.ts
@@ -16,9 +16,10 @@
import { describe, it, expect, afterEach, beforeEach, vi } from 'vitest';
import request from 'supertest';
-import express, { Express, Request, Response } from 'express';
-import authRoutes from '../../../src/service/routes/auth';
+import express, { Express } from 'express';
+import { RegisterRoutes } from '../../../src/service/generatedRoutes';
import * as db from '../../../src/db';
+import { ValidateError } from 'tsoa';
vi.mock('../../../src/db', () => ({
findUser: vi.fn(),
@@ -26,18 +27,46 @@ vi.mock('../../../src/db', () => ({
createUser: vi.fn(),
}));
-const newApp = (username?: string): Express => {
+/**
+ * Builds a minimal Express app with tsoa routes registered.
+ * When `username` is supplied the request is pre-authenticated via middleware,
+ * simulating a session-authenticated user.
+ */
+const newApp = (username?: string, isAdmin = false): Express => {
const app = express();
app.use(express.json());
if (username) {
app.use((req, _res, next) => {
- req.user = { username };
+ req.user = { username, admin: isAdmin };
+ next();
+ });
+ } else {
+ app.use((_req, _res, next) => {
next();
});
}
- app.use('/auth', authRoutes.router);
+ // Generic error handler so tsoa thrown errors propagate as HTTP responses.
+ RegisterRoutes(app);
+
+ // tsoa validation errors
+ app.use((err: any, _req: any, res: any, next: any) => {
+ if (err instanceof ValidateError) {
+ return res.status(400).json({
+ message: 'Validation failed',
+ details: err.fields,
+ });
+ }
+ next(err);
+ });
+
+ // Generic error handler so tsoa thrown errors propagate as HTTP responses.
+ app.use((err: any, _req: any, res: any, next: any) => {
+ if (res.headersSent) return next(err);
+ res.status(err.status ?? 500).json({ message: err.message });
+ });
+
return app;
};
@@ -46,7 +75,7 @@ describe('Auth API', () => {
vi.restoreAllMocks();
});
- describe('POST /gitAccount', () => {
+ describe('POST /api/auth/gitAccount', () => {
beforeEach(() => {
vi.mocked(db.findUser).mockImplementation((username: string) => {
if (username === 'alice') {
@@ -75,7 +104,7 @@ describe('Auth API', () => {
});
it('should return 401 Unauthorized if authenticated user not in request', async () => {
- const res = await request(newApp()).post('/auth/gitAccount').send({
+ const res = await request(newApp()).post('/api/auth/gitAccount').send({
username: 'alice',
gitAccount: '',
});
@@ -84,7 +113,7 @@ describe('Auth API', () => {
});
it('should return 400 Bad Request if username is missing', async () => {
- const res = await request(newApp('alice')).post('/auth/gitAccount').send({
+ const res = await request(newApp('alice', true)).post('/api/auth/gitAccount').send({
gitAccount: 'UPDATED_GIT_ACCOUNT',
});
@@ -92,7 +121,7 @@ describe('Auth API', () => {
});
it('should return 400 Bad Request if username is undefined', async () => {
- const res = await request(newApp('alice')).post('/auth/gitAccount').send({
+ const res = await request(newApp('alice', true)).post('/api/auth/gitAccount').send({
username: undefined,
gitAccount: 'UPDATED_GIT_ACCOUNT',
});
@@ -101,7 +130,7 @@ describe('Auth API', () => {
});
it('should return 400 Bad Request if username is null', async () => {
- const res = await request(newApp('alice')).post('/auth/gitAccount').send({
+ const res = await request(newApp('alice', true)).post('/api/auth/gitAccount').send({
username: null,
gitAccount: 'UPDATED_GIT_ACCOUNT',
});
@@ -110,7 +139,7 @@ describe('Auth API', () => {
});
it('should return 400 Bad Request if username is an empty string', async () => {
- const res = await request(newApp('alice')).post('/auth/gitAccount').send({
+ const res = await request(newApp('alice', true)).post('/api/auth/gitAccount').send({
username: '',
gitAccount: 'UPDATED_GIT_ACCOUNT',
});
@@ -119,7 +148,7 @@ describe('Auth API', () => {
});
it('should return 403 Forbidden if user is not an admin', async () => {
- const res = await request(newApp('bob')).post('/auth/gitAccount').send({
+ const res = await request(newApp('bob')).post('/api/auth/gitAccount').send({
username: 'alice',
gitAccount: 'UPDATED_GIT_ACCOUNT',
});
@@ -128,7 +157,7 @@ describe('Auth API', () => {
});
it('should return 404 Not Found if user is not found', async () => {
- const res = await request(newApp('alice')).post('/auth/gitAccount').send({
+ const res = await request(newApp('alice', true)).post('/api/auth/gitAccount').send({
username: 'non-existent-user',
gitAccount: 'UPDATED_GIT_ACCOUNT',
});
@@ -139,7 +168,7 @@ describe('Auth API', () => {
it('should return 200 OK if user is an admin and updates git account for authenticated user', async () => {
const updateUserSpy = vi.mocked(db.updateUser).mockResolvedValue();
- const res = await request(newApp('alice')).post('/auth/gitAccount').send({
+ const res = await request(newApp('alice', true)).post('/api/auth/gitAccount').send({
username: 'alice',
gitAccount: 'UPDATED_GIT_ACCOUNT',
});
@@ -160,7 +189,7 @@ describe('Auth API', () => {
it("should prevent non-admin users from changing a different user's gitAccount", async () => {
const updateUserSpy = vi.mocked(db.updateUser).mockResolvedValue();
- const res = await request(newApp('bob')).post('/auth/gitAccount').send({
+ const res = await request(newApp('bob')).post('/api/auth/gitAccount').send({
username: 'phil',
gitAccount: 'UPDATED_GIT_ACCOUNT',
});
@@ -172,7 +201,7 @@ describe('Auth API', () => {
it("should allow admin users to change a different user's gitAccount", async () => {
const updateUserSpy = vi.mocked(db.updateUser).mockResolvedValue();
- const res = await request(newApp('alice')).post('/auth/gitAccount').send({
+ const res = await request(newApp('alice', true)).post('/api/auth/gitAccount').send({
username: 'bob',
gitAccount: 'UPDATED_GIT_ACCOUNT',
});
@@ -193,7 +222,7 @@ describe('Auth API', () => {
it('should allow non-admin users to update their own gitAccount', async () => {
const updateUserSpy = vi.mocked(db.updateUser).mockResolvedValue();
- const res = await request(newApp('bob')).post('/auth/gitAccount').send({
+ const res = await request(newApp('bob')).post('/api/auth/gitAccount').send({
username: 'bob',
gitAccount: 'UPDATED_GIT_ACCOUNT',
});
@@ -212,46 +241,9 @@ describe('Auth API', () => {
});
});
- describe('loginSuccessHandler', () => {
- it('should log in user and return public user data', async () => {
- const user = {
- username: 'bob',
- password: 'secret',
- email: 'bob@example.com',
- displayName: 'Bob',
- admin: false,
- gitAccount: '',
- title: '',
- };
-
- const sendSpy = vi.fn();
- const res = {
- send: sendSpy,
- };
-
- await authRoutes.loginSuccessHandler()(
- { user } as unknown as Request,
- res as unknown as Response,
- );
-
- expect(sendSpy).toHaveBeenCalledOnce();
- expect(sendSpy).toHaveBeenCalledWith({
- message: 'success',
- user: {
- admin: false,
- displayName: 'Bob',
- email: 'bob@example.com',
- gitAccount: '',
- title: '',
- username: 'bob',
- },
- });
- });
- });
-
- describe('GET /profile', () => {
+ describe('GET /api/auth/profile', () => {
it('should return 401 Unauthorized if user is not logged in', async () => {
- const res = await request(newApp()).get('/auth/profile');
+ const res = await request(newApp()).get('/api/auth/profile');
expect(res.status).toBe(401);
});
@@ -267,7 +259,7 @@ describe('Auth API', () => {
title: '',
});
- const res = await request(newApp('alice')).get('/auth/profile');
+ const res = await request(newApp('alice')).get('/api/auth/profile');
expect(res.status).toBe(200);
expect(res.body).toEqual({
username: 'alice',
@@ -282,15 +274,15 @@ describe('Auth API', () => {
it('should return 404 Not Found if user is not found', async () => {
vi.mocked(db.findUser).mockResolvedValue(null);
- const res = await request(newApp('non-existent-user')).get('/auth/profile');
+ const res = await request(newApp('non-existent-user')).get('/api/auth/profile');
expect(res.status).toBe(404);
expect(res.body).toEqual({ message: 'User not found' });
});
});
- describe('GET /', () => {
+ describe('GET /api/auth', () => {
it('should return 200 OK and the auth endpoints', async () => {
- const res = await request(newApp()).get('/auth');
+ const res = await request(newApp()).get('/api/auth');
expect(res.status).toBe(200);
expect(res.body).toEqual({
login: {
@@ -309,9 +301,9 @@ describe('Auth API', () => {
});
});
- describe('GET /config', () => {
+ describe('GET /api/auth/config', () => {
it('should return 200 OK and the default auth config', async () => {
- const res = await request(newApp()).get('/auth/config');
+ const res = await request(newApp()).get('/api/auth/config');
expect(res.status).toBe(200);
expect(res.body).toEqual({
usernamePasswordMethod: 'local',
diff --git a/test/services/routes/config.test.ts b/test/services/routes/config.test.ts
index 4caa53758..536e3d7ce 100644
--- a/test/services/routes/config.test.ts
+++ b/test/services/routes/config.test.ts
@@ -17,7 +17,7 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import express, { Express } from 'express';
import request from 'supertest';
-import configRouter from '../../../src/service/routes/config';
+import { RegisterRoutes } from '../../../src/service/generatedRoutes';
import * as config from '../../../src/config';
describe('Config API', () => {
@@ -26,7 +26,7 @@ describe('Config API', () => {
beforeEach(() => {
app = express();
app.use(express.json());
- app.use('/config', configRouter);
+ RegisterRoutes(app);
vi.spyOn(config, 'getAttestationConfig').mockReturnValue({ questions: [] });
vi.spyOn(config, 'getURLShortener').mockReturnValue('https://url-shortener.com');
@@ -38,26 +38,27 @@ describe('Config API', () => {
vi.restoreAllMocks();
});
- it('GET /config/attestation should return 200 OK and the default attestation config', async () => {
- const res = await request(app).get('/config/attestation');
+ it('GET /api/v1/config/attestation should return 200 OK and the default attestation config', async () => {
+ const res = await request(app).get('/api/v1/config/attestation');
expect(res.status).toBe(200);
expect(res.body).toEqual({ questions: [] });
});
- it('GET /config/urlShortener should return 200 OK and the default url shortener config', async () => {
- const res = await request(app).get('/config/urlShortener');
+ it('GET /api/v1/config/urlShortener should return 200 OK and the url shortener config', async () => {
+ const res = await request(app).get('/api/v1/config/urlShortener');
expect(res.status).toBe(200);
- expect(res.text).toBe('https://url-shortener.com'); // Check res.text as it gets serialized as a string
+ // tsoa sends plain strings via res.send() so the value is in res.text, not res.body.
+ expect(res.text).toBe('https://url-shortener.com');
});
- it('GET /config/contactEmail should return 200 OK and the default contact email', async () => {
- const res = await request(app).get('/config/contactEmail');
+ it('GET /api/v1/config/contactEmail should return 200 OK and the contact email', async () => {
+ const res = await request(app).get('/api/v1/config/contactEmail');
expect(res.status).toBe(200);
expect(res.text).toBe('test@example.com');
});
- it('GET /config/uiRouteAuth should return 200 OK and the default ui route auth config', async () => {
- const res = await request(app).get('/config/uiRouteAuth');
+ it('GET /api/v1/config/uiRouteAuth should return 200 OK and the ui route auth config', async () => {
+ const res = await request(app).get('/api/v1/config/uiRouteAuth');
expect(res.status).toBe(200);
expect(res.body).toEqual({ enabled: false, rules: [] });
});
diff --git a/test/services/routes/healthCheck.test.ts b/test/services/routes/healthCheck.test.ts
index 122203210..c1e6c02c6 100644
--- a/test/services/routes/healthCheck.test.ts
+++ b/test/services/routes/healthCheck.test.ts
@@ -17,7 +17,7 @@
import { describe, it, expect, beforeEach } from 'vitest';
import express, { Express } from 'express';
import request from 'supertest';
-import healthcheck from '../../../src/service/routes/healthcheck';
+import { RegisterRoutes } from '../../../src/service/generatedRoutes';
describe('Health Check API', () => {
let app: Express;
@@ -25,11 +25,11 @@ describe('Health Check API', () => {
beforeEach(() => {
app = express();
app.use(express.json());
- app.use('/healthCheck', healthcheck);
+ RegisterRoutes(app);
});
- it('GET /healthCheck should return 200 OK and the health check message', async () => {
- const res = await request(app).get('/healthCheck');
+ it('GET /api/v1/healthcheck should return 200 OK and the health check message', async () => {
+ const res = await request(app).get('/api/v1/healthcheck');
expect(res.status).toBe(200);
expect(res.body).toEqual({ message: 'ok' });
});
diff --git a/test/services/routes/users.test.ts b/test/services/routes/users.test.ts
index 6df9e3a57..eaedfd5e6 100644
--- a/test/services/routes/users.test.ts
+++ b/test/services/routes/users.test.ts
@@ -17,18 +17,36 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import express, { Express } from 'express';
import request from 'supertest';
-import usersRouter from '../../../src/service/routes/users';
+import { RegisterRoutes } from '../../../src/service/generatedRoutes';
import * as db from '../../../src/db';
import { utils } from 'ssh2';
import crypto from 'crypto';
describe('Users API', () => {
let app: Express;
+ // The authenticated user for the current request. Tests reassign this to
+ // exercise different users; set it to undefined for the unauthenticated case.
+ let currentUser: { username: string; admin?: boolean } | undefined;
beforeEach(() => {
+ currentUser = { username: 'testuser' };
app = express();
app.use(express.json());
- app.use('/users', usersRouter);
+ app.use((req, _res, next) => {
+ // tsoa's @Security('jwt') middleware overwrites req.user with whatever
+ // expressAuthentication returns. With a session (isAuthenticated() === true)
+ // it returns the existing req.user, so mark the request as session-authenticated.
+ if (currentUser) {
+ req.user = currentUser;
+ (req as any).isAuthenticated = () => true;
+ }
+ next();
+ });
+ RegisterRoutes(app);
+ app.use((err: any, _req: any, res: any, next: any) => {
+ if (res.headersSent) return next(err);
+ res.status(err.status ?? 500).json({ message: err.message });
+ });
vi.spyOn(db, 'getUsers').mockResolvedValue([
{
@@ -55,8 +73,8 @@ describe('Users API', () => {
vi.restoreAllMocks();
});
- it('GET /users only serializes public data needed for ui, not user secrets like password', async () => {
- const res = await request(app).get('/users');
+ it('GET /api/v1/user only serializes public data needed for ui, not user secrets like password', async () => {
+ const res = await request(app).get('/api/v1/user');
expect(res.status).toBe(200);
expect(res.body).toEqual([
@@ -71,8 +89,8 @@ describe('Users API', () => {
]);
});
- it('GET /users/:id does not serialize password', async () => {
- const res = await request(app).get('/users/bob');
+ it('GET /api/v1/user/:id does not serialize password', async () => {
+ const res = await request(app).get('/api/v1/user/bob');
expect(res.status).toBe(200);
console.log(`Response body: ${JSON.stringify(res.body)}`);
@@ -102,39 +120,26 @@ describe('Users API', () => {
vi.spyOn(db, 'removePublicKey').mockResolvedValue(undefined);
});
- describe('GET /users/:username/ssh-key-fingerprints', () => {
+ describe('GET /api/v1/user/:username/ssh-key-fingerprints', () => {
it('should return 401 when not authenticated', async () => {
- const res = await request(app).get('/users/alice/ssh-key-fingerprints');
+ currentUser = undefined;
+ const res = await request(app).get('/api/v1/user/alice/ssh-key-fingerprints');
expect(res.status).toBe(401);
expect(res.body).toEqual({ error: 'Authentication required' });
});
it('should return 403 when non-admin tries to view other user keys', async () => {
- const testApp = express();
- testApp.use(express.json());
- testApp.use((req, res, next) => {
- req.user = { username: 'bob', admin: false };
- next();
- });
- testApp.use('/users', usersRouter);
-
- const res = await request(testApp).get('/users/alice/ssh-key-fingerprints');
+ currentUser = { username: 'bob', admin: false };
+ const res = await request(app).get('/api/v1/user/alice/ssh-key-fingerprints');
expect(res.status).toBe(403);
expect(res.body).toEqual({ error: 'Not authorized to view keys for this user' });
});
it('should allow user to view their own keys', async () => {
- const testApp = express();
- testApp.use(express.json());
- testApp.use((req, res, next) => {
- req.user = { username: 'alice', admin: false };
- next();
- });
- testApp.use('/users', usersRouter);
-
- const res = await request(testApp).get('/users/alice/ssh-key-fingerprints');
+ currentUser = { username: 'alice', admin: false };
+ const res = await request(app).get('/api/v1/user/alice/ssh-key-fingerprints');
expect(res.status).toBe(200);
expect(res.body).toEqual([
@@ -147,15 +152,8 @@ describe('Users API', () => {
});
it('should allow admin to view any user keys', async () => {
- const testApp = express();
- testApp.use(express.json());
- testApp.use((req, res, next) => {
- req.user = { username: 'admin', admin: true };
- next();
- });
- testApp.use('/users', usersRouter);
-
- const res = await request(testApp).get('/users/alice/ssh-key-fingerprints');
+ currentUser = { username: 'admin', admin: true };
+ const res = await request(app).get('/api/v1/user/alice/ssh-key-fingerprints');
expect(res.status).toBe(200);
expect(db.getPublicKeys).toHaveBeenCalledWith('alice');
@@ -164,22 +162,15 @@ describe('Users API', () => {
it('should handle errors when retrieving keys', async () => {
vi.spyOn(db, 'getPublicKeys').mockRejectedValue(new Error('Database error'));
- const testApp = express();
- testApp.use(express.json());
- testApp.use((req, res, next) => {
- req.user = { username: 'alice', admin: false };
- next();
- });
- testApp.use('/users', usersRouter);
-
- const res = await request(testApp).get('/users/alice/ssh-key-fingerprints');
+ currentUser = { username: 'alice', admin: false };
+ const res = await request(app).get('/api/v1/user/alice/ssh-key-fingerprints');
expect(res.status).toBe(500);
expect(res.body).toEqual({ error: 'Failed to retrieve SSH keys' });
});
});
- describe('POST /users/:username/ssh-keys', () => {
+ describe('POST /api/v1/user/:username/ssh-keys', () => {
const validPublicKey = 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAITest test@example.com';
beforeEach(() => {
@@ -195,8 +186,9 @@ describe('Users API', () => {
});
it('should return 401 when not authenticated', async () => {
+ currentUser = undefined;
const res = await request(app)
- .post('/users/alice/ssh-keys')
+ .post('/api/v1/user/alice/ssh-keys')
.send({ publicKey: validPublicKey });
expect(res.status).toBe(401);
@@ -204,16 +196,9 @@ describe('Users API', () => {
});
it('should return 403 when non-admin tries to add key for other user', async () => {
- const testApp = express();
- testApp.use(express.json());
- testApp.use((req, res, next) => {
- req.user = { username: 'bob', admin: false };
- next();
- });
- testApp.use('/users', usersRouter);
-
- const res = await request(testApp)
- .post('/users/alice/ssh-keys')
+ currentUser = { username: 'bob', admin: false };
+ const res = await request(app)
+ .post('/api/v1/user/alice/ssh-keys')
.send({ publicKey: validPublicKey });
expect(res.status).toBe(403);
@@ -221,15 +206,8 @@ describe('Users API', () => {
});
it('should return 400 when public key is missing', async () => {
- const testApp = express();
- testApp.use(express.json());
- testApp.use((req, res, next) => {
- req.user = { username: 'alice', admin: false };
- next();
- });
- testApp.use('/users', usersRouter);
-
- const res = await request(testApp).post('/users/alice/ssh-keys').send({});
+ currentUser = { username: 'alice', admin: false };
+ const res = await request(app).post('/api/v1/user/alice/ssh-keys').send({});
expect(res.status).toBe(400);
expect(res.body).toEqual({ error: 'Public key is required' });
@@ -238,16 +216,9 @@ describe('Users API', () => {
it('should return 400 when public key format is invalid', async () => {
vi.spyOn(utils, 'parseKey').mockReturnValue(null as any);
- const testApp = express();
- testApp.use(express.json());
- testApp.use((req, res, next) => {
- req.user = { username: 'alice', admin: false };
- next();
- });
- testApp.use('/users', usersRouter);
-
- const res = await request(testApp)
- .post('/users/alice/ssh-keys')
+ currentUser = { username: 'alice', admin: false };
+ const res = await request(app)
+ .post('/api/v1/user/alice/ssh-keys')
.send({ publicKey: 'invalid-key' });
expect(res.status).toBe(400);
@@ -255,16 +226,9 @@ describe('Users API', () => {
});
it('should successfully add SSH key', async () => {
- const testApp = express();
- testApp.use(express.json());
- testApp.use((req, res, next) => {
- req.user = { username: 'alice', admin: false };
- next();
- });
- testApp.use('/users', usersRouter);
-
- const res = await request(testApp)
- .post('/users/alice/ssh-keys')
+ currentUser = { username: 'alice', admin: false };
+ const res = await request(app)
+ .post('/api/v1/user/alice/ssh-keys')
.send({ publicKey: validPublicKey, name: 'My Key' });
expect(res.status).toBe(201);
@@ -282,16 +246,9 @@ describe('Users API', () => {
});
it('should use default name when name not provided', async () => {
- const testApp = express();
- testApp.use(express.json());
- testApp.use((req, res, next) => {
- req.user = { username: 'alice', admin: false };
- next();
- });
- testApp.use('/users', usersRouter);
-
- const res = await request(testApp)
- .post('/users/alice/ssh-keys')
+ currentUser = { username: 'alice', admin: false };
+ const res = await request(app)
+ .post('/api/v1/user/alice/ssh-keys')
.send({ publicKey: validPublicKey });
expect(res.status).toBe(201);
@@ -306,16 +263,9 @@ describe('Users API', () => {
it('should return 409 when key already exists', async () => {
vi.spyOn(db, 'addPublicKey').mockRejectedValue(new Error('SSH key already exists'));
- const testApp = express();
- testApp.use(express.json());
- testApp.use((req, res, next) => {
- req.user = { username: 'alice', admin: false };
- next();
- });
- testApp.use('/users', usersRouter);
-
- const res = await request(testApp)
- .post('/users/alice/ssh-keys')
+ currentUser = { username: 'alice', admin: false };
+ const res = await request(app)
+ .post('/api/v1/user/alice/ssh-keys')
.send({ publicKey: validPublicKey });
expect(res.status).toBe(409);
@@ -325,16 +275,9 @@ describe('Users API', () => {
it('should return 404 when user not found', async () => {
vi.spyOn(db, 'addPublicKey').mockRejectedValue(new Error('User not found'));
- const testApp = express();
- testApp.use(express.json());
- testApp.use((req, res, next) => {
- req.user = { username: 'alice', admin: false };
- next();
- });
- testApp.use('/users', usersRouter);
-
- const res = await request(testApp)
- .post('/users/alice/ssh-keys')
+ currentUser = { username: 'alice', admin: false };
+ const res = await request(app)
+ .post('/api/v1/user/alice/ssh-keys')
.send({ publicKey: validPublicKey });
expect(res.status).toBe(404);
@@ -344,16 +287,9 @@ describe('Users API', () => {
it('should return 500 for other errors', async () => {
vi.spyOn(db, 'addPublicKey').mockRejectedValue(new Error('Database error'));
- const testApp = express();
- testApp.use(express.json());
- testApp.use((req, res, next) => {
- req.user = { username: 'alice', admin: false };
- next();
- });
- testApp.use('/users', usersRouter);
-
- const res = await request(testApp)
- .post('/users/alice/ssh-keys')
+ currentUser = { username: 'alice', admin: false };
+ const res = await request(app)
+ .post('/api/v1/user/alice/ssh-keys')
.send({ publicKey: validPublicKey });
expect(res.status).toBe(500);
@@ -361,16 +297,9 @@ describe('Users API', () => {
});
it('should allow admin to add key for any user', async () => {
- const testApp = express();
- testApp.use(express.json());
- testApp.use((req, res, next) => {
- req.user = { username: 'admin', admin: true };
- next();
- });
- testApp.use('/users', usersRouter);
-
- const res = await request(testApp)
- .post('/users/alice/ssh-keys')
+ currentUser = { username: 'admin', admin: true };
+ const res = await request(app)
+ .post('/api/v1/user/alice/ssh-keys')
.send({ publicKey: validPublicKey });
expect(res.status).toBe(201);
@@ -378,39 +307,26 @@ describe('Users API', () => {
});
});
- describe('DELETE /users/:username/ssh-keys/:fingerprint', () => {
+ describe('DELETE /api/v1/user/:username/ssh-keys/:fingerprint', () => {
it('should return 401 when not authenticated', async () => {
- const res = await request(app).delete('/users/alice/ssh-keys/SHA256:test123');
+ currentUser = undefined;
+ const res = await request(app).delete('/api/v1/user/alice/ssh-keys/SHA256:test123');
expect(res.status).toBe(401);
expect(res.body).toEqual({ error: 'Authentication required' });
});
it('should return 403 when non-admin tries to remove key for other user', async () => {
- const testApp = express();
- testApp.use(express.json());
- testApp.use((req, res, next) => {
- req.user = { username: 'bob', admin: false };
- next();
- });
- testApp.use('/users', usersRouter);
-
- const res = await request(testApp).delete('/users/alice/ssh-keys/SHA256:test123');
+ currentUser = { username: 'bob', admin: false };
+ const res = await request(app).delete('/api/v1/user/alice/ssh-keys/SHA256:test123');
expect(res.status).toBe(403);
expect(res.body).toEqual({ error: 'Not authorized to remove keys for this user' });
});
it('should successfully remove SSH key', async () => {
- const testApp = express();
- testApp.use(express.json());
- testApp.use((req, res, next) => {
- req.user = { username: 'alice', admin: false };
- next();
- });
- testApp.use('/users', usersRouter);
-
- const res = await request(testApp).delete('/users/alice/ssh-keys/SHA256:test123');
+ currentUser = { username: 'alice', admin: false };
+ const res = await request(app).delete('/api/v1/user/alice/ssh-keys/SHA256:test123');
expect(res.status).toBe(200);
expect(res.body).toEqual({ message: 'SSH key removed successfully' });
@@ -420,15 +336,8 @@ describe('Users API', () => {
it('should return 404 when user not found', async () => {
vi.spyOn(db, 'removePublicKey').mockRejectedValue(new Error('User not found'));
- const testApp = express();
- testApp.use(express.json());
- testApp.use((req, res, next) => {
- req.user = { username: 'alice', admin: false };
- next();
- });
- testApp.use('/users', usersRouter);
-
- const res = await request(testApp).delete('/users/alice/ssh-keys/SHA256:test123');
+ currentUser = { username: 'alice', admin: false };
+ const res = await request(app).delete('/api/v1/user/alice/ssh-keys/SHA256:test123');
expect(res.status).toBe(404);
expect(res.body).toEqual({ error: 'User not found' });
@@ -437,30 +346,16 @@ describe('Users API', () => {
it('should return 500 for other errors', async () => {
vi.spyOn(db, 'removePublicKey').mockRejectedValue(new Error('Database error'));
- const testApp = express();
- testApp.use(express.json());
- testApp.use((req, res, next) => {
- req.user = { username: 'alice', admin: false };
- next();
- });
- testApp.use('/users', usersRouter);
-
- const res = await request(testApp).delete('/users/alice/ssh-keys/SHA256:test123');
+ currentUser = { username: 'alice', admin: false };
+ const res = await request(app).delete('/api/v1/user/alice/ssh-keys/SHA256:test123');
expect(res.status).toBe(500);
expect(res.body).toEqual({ error: 'Database error' });
});
it('should allow admin to remove key for any user', async () => {
- const testApp = express();
- testApp.use(express.json());
- testApp.use((req, res, next) => {
- req.user = { username: 'admin', admin: true };
- next();
- });
- testApp.use('/users', usersRouter);
-
- const res = await request(testApp).delete('/users/alice/ssh-keys/SHA256:test123');
+ currentUser = { username: 'admin', admin: true };
+ const res = await request(app).delete('/api/v1/user/alice/ssh-keys/SHA256:test123');
expect(res.status).toBe(200);
expect(db.removePublicKey).toHaveBeenCalledWith('alice', 'SHA256:test123');
@@ -468,10 +363,10 @@ describe('Users API', () => {
});
});
- it('GET /users/:id should return 404 Not Found if user is not found', async () => {
+ it('GET /api/v1/user/:id should return 404 Not Found if user is not found', async () => {
vi.restoreAllMocks();
- const res = await request(app).get('/users/non-existent');
+ const res = await request(app).get('/api/v1/user/non-existent');
expect(res.status).toBe(404);
expect(res.body).toEqual({ message: 'User non-existent not found' });
});
diff --git a/test/testJwtAuthHandler.test.ts b/test/testJwtAuthHandler.test.ts
index 7d41872e1..b2d14fade 100644
--- a/test/testJwtAuthHandler.test.ts
+++ b/test/testJwtAuthHandler.test.ts
@@ -16,12 +16,12 @@
import axios from 'axios';
import crypto from 'crypto';
-import { NextFunction } from 'express';
import jwt, { JwtPayload } from 'jsonwebtoken';
import { describe, it, expect, vi, beforeEach, afterEach, MockInstance } from 'vitest';
import { assignRoles, getJwks, validateJwt } from '../src/service/passport/jwtUtils';
-import { jwtAuthHandler } from '../src/service/passport/jwtAuthHandler';
+import { expressAuthentication } from '../src/service/authentication';
+import * as configModule from '../src/config';
import { JwtConfig, RoleMapping } from '../src/config/generated/config';
function generateRsaKeyPair() {
@@ -189,17 +189,13 @@ describe('JWT', () => {
});
});
- describe('jwtAuthHandler', () => {
+ describe('expressAuthentication', () => {
let req: any;
- let res: any;
- let next: NextFunction;
let jwtConfig: JwtConfig;
- let validVerifyResponse: JwtPayload;
+ let mockGetAPIAuthMethods: MockInstance;
beforeEach(() => {
req = { header: vi.fn(), isAuthenticated: vi.fn(), user: {} };
- res = { status: vi.fn().mockReturnThis(), send: vi.fn() };
- next = vi.fn();
jwtConfig = {
clientID: 'client-id',
@@ -208,62 +204,57 @@ describe('JWT', () => {
roleMapping: { admin: { admin: 'admin' } },
};
- validVerifyResponse = {
- header: { kid: '123' },
- azp: 'client-id',
- sub: 'user123',
- admin: 'admin',
- };
+ mockGetAPIAuthMethods = vi
+ .spyOn(configModule, 'getAPIAuthMethods')
+ .mockReturnValue([{ type: 'jwt', enabled: true, jwtConfig }] as any);
});
afterEach(() => vi.restoreAllMocks());
- it('should call next if user is authenticated', async () => {
+ it('should return user if already authenticated via session', async () => {
req.isAuthenticated.mockReturnValue(true);
- await jwtAuthHandler()(req, res, next);
- expect(next).toHaveBeenCalledOnce();
+ const result = await expressAuthentication(req, 'jwt');
+ expect(result).toBe(req.user);
});
- it('should return 401 if no token provided', async () => {
- req.header.mockReturnValue(null);
- await jwtAuthHandler(jwtConfig)(req, res, next);
+ it('should return undefined if JWT auth method is not configured', async () => {
+ mockGetAPIAuthMethods.mockReturnValue([]);
+ req.isAuthenticated.mockReturnValue(false);
+ const result = await expressAuthentication(req, 'jwt');
+ expect(result).toBeUndefined();
+ });
- expect(res.status).toHaveBeenCalledWith(401);
- expect(res.send).toHaveBeenCalledWith('No token provided\n');
+ it('should throw 401 if no token provided', async () => {
+ req.isAuthenticated.mockReturnValue(false);
+ req.header.mockReturnValue(null);
+ await expect(expressAuthentication(req, 'jwt')).rejects.toMatchObject({ status: 401 });
});
- it('should return 500 if authorityURL not configured', async () => {
+ it('should throw 500 if authorityURL not configured', async () => {
+ req.isAuthenticated.mockReturnValue(false);
req.header.mockReturnValue('Bearer fake-token');
jwtConfig.authorityURL = null;
- vi.spyOn(jwt, 'verify').mockReturnValue(validVerifyResponse);
-
- await jwtAuthHandler(jwtConfig)(req, res, next);
-
- expect(res.status).toHaveBeenCalledWith(500);
- expect(res.send).toHaveBeenCalledWith({ message: 'OIDC authority URL is not configured\n' });
+ await expect(expressAuthentication(req, 'jwt')).rejects.toMatchObject({ status: 500 });
});
- it('should return 500 if clientID not configured', async () => {
+ it('should throw 500 if clientID not configured', async () => {
+ req.isAuthenticated.mockReturnValue(false);
req.header.mockReturnValue('Bearer fake-token');
jwtConfig.clientID = null;
- vi.spyOn(jwt, 'verify').mockReturnValue(validVerifyResponse);
-
- await jwtAuthHandler(jwtConfig)(req, res, next);
-
- expect(res.status).toHaveBeenCalledWith(500);
- expect(res.send).toHaveBeenCalledWith({ message: 'OIDC client ID is not configured\n' });
+ await expect(expressAuthentication(req, 'jwt')).rejects.toMatchObject({ status: 500 });
});
- it('should return 401 if JWT validation fails', async () => {
+ it('should throw 401 if JWT validation fails', async () => {
+ req.isAuthenticated.mockReturnValue(false);
req.header.mockReturnValue('Bearer fake-token');
vi.spyOn(jwt, 'verify').mockImplementation(() => {
throw new Error('Invalid token');
});
+ await expect(expressAuthentication(req, 'jwt')).rejects.toMatchObject({ status: 401 });
+ });
- await jwtAuthHandler(jwtConfig)(req, res, next);
-
- expect(res.status).toHaveBeenCalledWith(401);
- expect(res.send).toHaveBeenCalledWith(expect.stringMatching(/Invalid JWT:/));
+ it('should throw 401 for unknown security scheme', async () => {
+ await expect(expressAuthentication(req, 'unknown')).rejects.toMatchObject({ status: 401 });
});
});
});
diff --git a/test/testLogin.test.ts b/test/testLogin.test.ts
index 07ec1d2ba..472a253aa 100644
--- a/test/testLogin.test.ts
+++ b/test/testLogin.test.ts
@@ -197,9 +197,10 @@ describe('login', () => {
});
expect(res.status).toBe(400);
- expect(res.body.message).toBe(
- 'Missing required fields: username, password, email, and gitAccount are required',
- );
+ // tsoa validates required body fields before the controller runs.
+ // The 'password' field is missing; tsoa prefixes body field errors with 'body.'.
+ expect(res.body.message).toBe('Validation failed');
+ expect(res.body.details).toHaveProperty('body.password');
});
it('should successfully create a new user', async () => {
diff --git a/test/testProxyRoute.test.ts b/test/testProxyRoute.test.ts
index 32f720015..63360e78f 100644
--- a/test/testProxyRoute.test.ts
+++ b/test/testProxyRoute.test.ts
@@ -58,6 +58,14 @@ const TEST_UNKNOWN_REPO = {
fallbackUrlPrefix: '/finos/fdc3.git',
};
+// `host` / `proxyUrlPrefix` / `fallbackUrlPrefix` are test-only helpers; the
+// API rejects them as unknown properties on CreateRepoBody.
+const toCreateBody = ({ url, name, project }: { url: string; name: string; project: string }) => ({
+ url,
+ name,
+ project,
+});
+
afterAll(() => {
vi.resetModules();
});
@@ -105,7 +113,7 @@ describe('proxy express application', () => {
const res2 = await request(apiApp)
.post('/api/v1/repo')
.set('Cookie', cookie)
- .send(TEST_DEFAULT_REPO);
+ .send(toCreateBody(TEST_DEFAULT_REPO));
expect(res2.status).toBe(200);
}
});
@@ -122,7 +130,10 @@ describe('proxy express application', () => {
// Ensure default repo exists
const repo = await db.getRepoByUrl(TEST_DEFAULT_REPO.url);
if (!repo) {
- await request(apiApp).post('/api/v1/repo').set('Cookie', cookie).send(TEST_DEFAULT_REPO);
+ await request(apiApp)
+ .post('/api/v1/repo')
+ .set('Cookie', cookie)
+ .send(toCreateBody(TEST_DEFAULT_REPO));
}
// proxy a fetch request
@@ -160,7 +171,7 @@ describe('proxy express application', () => {
const res = await request(apiApp)
.post('/api/v1/repo')
.set('Cookie', cookie)
- .send(TEST_GITLAB_REPO);
+ .send(toCreateBody(TEST_GITLAB_REPO));
expect(res.status).toBe(200);
// confirm that the repo was created in the DB
@@ -188,7 +199,10 @@ describe('proxy express application', () => {
// Ensure the gitlab test repo exists (create it if a previous test didn't)
let repo = await db.getRepoByUrl(TEST_GITLAB_REPO.url);
if (!repo) {
- await request(apiApp).post('/api/v1/repo').set('Cookie', cookie).send(TEST_GITLAB_REPO);
+ await request(apiApp)
+ .post('/api/v1/repo')
+ .set('Cookie', cookie)
+ .send(toCreateBody(TEST_GITLAB_REPO));
repo = await db.getRepoByUrl(TEST_GITLAB_REPO.url);
}
expect(repo).not.toBeNull();
diff --git a/test/testPush.test.ts b/test/testPush.test.ts
index 8bf85788d..622d4dc19 100644
--- a/test/testPush.test.ts
+++ b/test/testPush.test.ts
@@ -123,7 +123,7 @@ describe('Push API', () => {
await db.deletePush(TEST_PUSH.id);
vi.resetModules();
- await Service.httpServer.close();
+ await Service.httpServer?.close();
const res = await request(app).post('/api/auth/logout').set('Cookie', `${cookie}`);
expect(res.status).toBe(200);
@@ -162,10 +162,6 @@ describe('Push API', () => {
attestation: [
{
label: 'I am happy for this to be pushed to the upstream repository',
- tooltip: {
- text: 'Are you happy for this contribution to be pushed upstream?',
- links: [],
- },
checked: true,
},
],
@@ -190,10 +186,6 @@ describe('Push API', () => {
attestation: [
{
label: 'I am happy for this to be pushed to the upstream repository',
- tooltip: {
- text: 'Are you happy for this contribution to be pushed upstream?',
- links: [],
- },
checked: false,
},
],
@@ -219,10 +211,6 @@ describe('Push API', () => {
attestation: [
{
label: 'I am happy for this to be pushed to the upstream repository',
- tooltip: {
- text: 'Are you happy for this contribution to be pushed upstream?',
- links: [],
- },
checked: true,
},
],
@@ -251,10 +239,6 @@ describe('Push API', () => {
attestation: [
{
label: 'I am happy for this to be pushed to the upstream repository',
- tooltip: {
- text: 'Are you happy for this contribution to be pushed upstream?',
- links: [],
- },
checked: true,
},
],
@@ -276,10 +260,6 @@ describe('Push API', () => {
attestation: [
{
label: 'I am happy for this to be pushed to the upstream repository',
- tooltip: {
- text: 'Are you happy for this contribution to be pushed upstream?',
- links: [],
- },
checked: true,
},
],
@@ -292,7 +272,7 @@ describe('Push API', () => {
it('should return 401 if not logged in when approving a push', async () => {
const res = await request(app)
.post(`/api/v1/push/${TEST_PUSH.id}/authorise`)
- .send({ reason: 'Testing approval' });
+ .send({ params: { attestation: [] } });
expect(res.status).toBe(401);
expect(res.body.message).toBe('Not logged in');
});
@@ -315,7 +295,11 @@ describe('Push API', () => {
.set('Cookie', `${cookie}`)
.send({});
expect(res.status).toBe(400);
- expect(res.body.message).toBe('Rejection reason is required');
+ expect(res.body.message).toBe('Validation failed');
+ expect(res.body.details).toBeDefined();
+ console.log(res.body.details);
+ expect(res.body.details['body.reason']).toBeDefined();
+ expect(res.body.details['body.reason'].message).toBe("'reason' is required");
});
it('should NOT allow an authorizer to reject a push with empty reason', async () => {
@@ -404,10 +388,6 @@ describe('Push API', () => {
attestation: [
{
label: 'I am happy for this to be pushed to the upstream repository',
- tooltip: {
- text: 'Are you happy for this contribution to be pushed upstream?',
- links: [],
- },
checked: true,
},
],
@@ -429,10 +409,6 @@ describe('Push API', () => {
attestation: [
{
label: 'I am happy for this to be pushed to the upstream repository',
- tooltip: {
- text: 'Are you happy for this contribution to be pushed upstream?',
- links: [],
- },
checked: true,
},
],
diff --git a/test/testRepoApi.test.ts b/test/testRepoApi.test.ts
index 11e4cf6bf..7d0d4c60b 100644
--- a/test/testRepoApi.test.ts
+++ b/test/testRepoApi.test.ts
@@ -44,6 +44,13 @@ const TEST_REPO_NAKED = {
host: '123.456.789:80',
};
+// `host` is only kept on the fixtures above as an expected value for
+// getAllProxiedHosts() assertions; the API rejects it as an unknown property.
+const toCreateBody = (repo: T): Omit => {
+ const { host: _host, ...rest } = repo;
+ return rest;
+};
+
const cleanupRepo = async (url: string) => {
const repo = await db.getRepoByUrl(url);
if (repo) {
@@ -77,7 +84,10 @@ describe('add new repo', () => {
const ensureTestRepoExists = async () => {
let repo = await db.getRepoByUrl(TEST_REPO.url);
if (!repo) {
- await request(app).post('/api/v1/repo').set('Cookie', `${cookie}`).send(TEST_REPO);
+ await request(app)
+ .post('/api/v1/repo')
+ .set('Cookie', `${cookie}`)
+ .send(toCreateBody(TEST_REPO));
repo = await db.getRepoByUrl(TEST_REPO.url);
}
if (repo) repoIds[0] = repo._id!;
@@ -124,7 +134,10 @@ describe('add new repo', () => {
// Ensure repo doesn't exist
await cleanupRepo(TEST_REPO.url);
- const res = await request(app).post('/api/v1/repo').set('Cookie', `${cookie}`).send(TEST_REPO);
+ const res = await request(app)
+ .post('/api/v1/repo')
+ .set('Cookie', `${cookie}`)
+ .send(toCreateBody(TEST_REPO));
expect(res.status).toBe(200);
const repo = await fetchRepoOrThrow(TEST_REPO.url);
@@ -155,7 +168,10 @@ describe('add new repo', () => {
it('return a 409 error if the repo already exists', async () => {
await ensureTestRepoExists();
- const res = await request(app).post('/api/v1/repo').set('Cookie', `${cookie}`).send(TEST_REPO);
+ const res = await request(app)
+ .post('/api/v1/repo')
+ .set('Cookie', `${cookie}`)
+ .send(toCreateBody(TEST_REPO));
expect(res.status).toBe(409);
expect(res.body.message).toBe('Repository ' + TEST_REPO.url + ' already exists!');
});
@@ -349,7 +365,7 @@ describe('add new repo', () => {
const res = await request(app)
.post('/api/v1/repo')
.set('Cookie', cookie)
- .send(TEST_REPO_NON_GITHUB);
+ .send(toCreateBody(TEST_REPO_NON_GITHUB));
expect(res.status).toBe(200);
const repo = await fetchRepoOrThrow(TEST_REPO_NON_GITHUB.url);
@@ -367,7 +383,7 @@ describe('add new repo', () => {
const res2 = await request(app)
.post('/api/v1/repo')
.set('Cookie', cookie)
- .send(TEST_REPO_NAKED);
+ .send(toCreateBody(TEST_REPO_NAKED));
expect(res2.status).toBe(200);
const repo2 = await fetchRepoOrThrow(TEST_REPO_NAKED.url);
@@ -383,14 +399,20 @@ describe('add new repo', () => {
// Ensure repos exist before deleting
let repo1 = await db.getRepoByUrl(TEST_REPO_NON_GITHUB.url);
if (!repo1) {
- await request(app).post('/api/v1/repo').set('Cookie', cookie).send(TEST_REPO_NON_GITHUB);
+ await request(app)
+ .post('/api/v1/repo')
+ .set('Cookie', cookie)
+ .send(toCreateBody(TEST_REPO_NON_GITHUB));
repo1 = await db.getRepoByUrl(TEST_REPO_NON_GITHUB.url);
}
repoIds[1] = repo1!._id!;
let repo2 = await db.getRepoByUrl(TEST_REPO_NAKED.url);
if (!repo2) {
- await request(app).post('/api/v1/repo').set('Cookie', cookie).send(TEST_REPO_NAKED);
+ await request(app)
+ .post('/api/v1/repo')
+ .set('Cookie', cookie)
+ .send(toCreateBody(TEST_REPO_NAKED));
repo2 = await db.getRepoByUrl(TEST_REPO_NAKED.url);
}
repoIds[2] = repo2!._id!;
@@ -466,7 +488,10 @@ describe('repo routes - edge cases', () => {
setCookie(nonAdminRes, 'nonAdmin');
// Create a test repo
- await request(app).post('/api/v1/repo').set('Cookie', adminCookie).send(TEST_REPO);
+ await request(app)
+ .post('/api/v1/repo')
+ .set('Cookie', adminCookie)
+ .send(toCreateBody(TEST_REPO));
const repo = await fetchRepoOrThrow(TEST_REPO.url);
repoId = repo._id!;
@@ -477,7 +502,6 @@ describe('repo routes - edge cases', () => {
url: 'https://github.com/test/unauthorized-repo.git',
name: 'unauthorized-repo',
project: 'test',
- host: 'github.com',
});
expect(res.status).toBe(401);
@@ -489,7 +513,6 @@ describe('repo routes - edge cases', () => {
url: 'https://github.com/test/unauthenticated-repo.git',
name: 'unauthenticated-repo',
project: 'test',
- host: 'github.com',
});
expect(res.status).toBe(401);
@@ -500,11 +523,13 @@ describe('repo routes - edge cases', () => {
const res = await request(app).post('/api/v1/repo').set('Cookie', adminCookie).send({
name: 'no-url-repo',
project: 'test',
- host: 'github.com',
});
expect(res.status).toBe(400);
- expect(res.body.message).toBe('Repository url is required');
+ expect(res.body.message).toBe('Validation failed');
+ expect(res.body.details).toBeDefined();
+ expect(res.body.details['body.url']).toBeDefined();
+ expect(res.body.details['body.url'].message).toBe("'url' is required");
});
it('should return 400 when repo url is invalid', async () => {
@@ -512,7 +537,6 @@ describe('repo routes - edge cases', () => {
url: '',
name: 'invalid-repo',
project: 'test',
- host: 'github.com',
});
expect(res.status).toBe(400);
expect(res.body.message).toBe('Repository url is required');
@@ -678,6 +702,6 @@ describe('repo routes - edge cases', () => {
await cleanupRepo(TEST_REPO.url);
await db.deleteUser('testuser');
await db.deleteUser('nonadmin');
- await Service.httpServer.close();
+ await Service.httpServer?.close();
});
});
diff --git a/tests/e2e/push.test.ts b/tests/e2e/push.test.ts
index a824af82a..9fbf6fdb0 100644
--- a/tests/e2e/push.test.ts
+++ b/tests/e2e/push.test.ts
@@ -674,7 +674,6 @@ describe('Git Proxy E2E - Repository Push Tests', () => {
const defaultQuestions = [
{
label: 'I am happy for this to be pushed to the upstream repository',
- tooltip: { label: 'test' },
checked: 'true',
},
];
diff --git a/tsconfig.json b/tsconfig.json
index 331c876ef..6ba296d3b 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -19,7 +19,9 @@
"outDir": "./dist",
"rootDir": "./src",
"noEmit": false,
- "types": ["node"]
+ "types": ["node"],
+
+ "experimentalDecorators": true
},
"include": ["src"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
diff --git a/tsoa.json b/tsoa.json
new file mode 100644
index 000000000..b58265054
--- /dev/null
+++ b/tsoa.json
@@ -0,0 +1,26 @@
+{
+ "entryFile": "src/app.ts",
+ "noImplicitAdditionalProperties": "throw-on-extras",
+ "controllerPathGlobs": ["src/service/controllers/**/*Controller.ts"],
+ "spec": {
+ "outputDirectory": "dist",
+ "specVersion": 3,
+ "securityDefinitions": {
+ "jwt": {
+ "type": "http",
+ "scheme": "bearer",
+ "bearerFormat": "JWT"
+ }
+ },
+ "info": {
+ "title": "Git Proxy API",
+ "version": "1.0.0"
+ }
+ },
+ "routes": {
+ "routesDir": "src/service",
+ "routesFileName": "generatedRoutes.ts",
+ "authenticationModule": "./src/service/authentication",
+ "middleware": "express"
+ }
+}
diff --git a/website/docs/api/auth.mdx b/website/docs/api/auth.mdx
new file mode 100644
index 000000000..a3ffce842
--- /dev/null
+++ b/website/docs/api/auth.mdx
@@ -0,0 +1,139 @@
+---
+title: Auth
+description: Auth API endpoints for GitProxy
+---
+
+# Auth
+
+*10 endpoints — 6 GET, 4 POST*
+
+## GET `/api/auth`
+
+Returns links to the available authentication resource endpoints.
+
+#### Responses
+
+- 200 Ok
+
+---
+
+## GET `/api/auth/config`
+
+Returns the enabled authentication methods available to the UI.
+
+#### Responses
+
+- 200 Ok
+
+---
+
+## GET `/api/auth/csrf-token`
+
+Returns a CSRF token for the current session, to be sent with subsequent
+state-changing requests.
+
+#### Responses
+
+- 200 Ok
+
+---
+
+## GET `/api/auth/openidconnect`
+
+Initiates the OpenID Connect authentication flow (redirects to the OIDC provider).
+
+#### Responses
+
+- 204 No content
+
+---
+
+## GET `/api/auth/openidconnect/callback`
+
+OpenID Connect callback — exchanges the authorization code for a session.
+
+#### Responses
+
+- 204 No content
+
+---
+
+## GET `/api/auth/profile`
+
+Returns the profile of the currently authenticated user.
+
+#### Responses
+
+- 200 Ok
+- 401 Unauthorized
+- 404 Not Found
+
+---
+
+## POST `/api/auth/create-user`
+
+Creates a new user. Requires admin privileges.
+
+#### Request Body
+
+| Field | Type | Required | Description |
+|-------|------|:--------:|-------------|
+| `username` | `string` | **Yes** | |
+| `password` | `string` | **Yes** | |
+| `email` | `string` | **Yes** | |
+| `gitAccount` | `string` | **Yes** | |
+| `admin` | `boolean` | No | |
+
+#### Responses
+
+- 200 Ok
+- 400 Bad Request
+- 403 Forbidden
+- 500 Internal Server Error
+
+---
+
+## POST `/api/auth/gitAccount`
+
+Updates the Git account (username) of a user.
+Admins may update any user; non-admins may only update their own account.
+
+#### Request Body
+
+| Field | Type | Required | Description |
+|-------|------|:--------:|-------------|
+| `username` | `string` | No | |
+| `id` | `string` | No | |
+| `gitAccount` | `string` | **Yes** | |
+
+#### Responses
+
+- 204 No content
+- 400 Bad Request
+- 401 Unauthorized
+- 403 Forbidden
+- 404 Not Found
+- 500 Internal Server Error
+
+---
+
+## POST `/api/auth/login`
+
+Authenticates the user with a username/password strategy.
+The appropriate passport strategy is selected dynamically based on configuration.
+
+#### Responses
+
+- 200 Ok
+
+---
+
+## POST `/api/auth/logout`
+
+Logs out the current user and clears the session cookie.
+
+#### Responses
+
+- 200 Ok
+
+---
diff --git a/website/docs/api/config.mdx b/website/docs/api/config.mdx
new file mode 100644
index 000000000..5dfbc4830
--- /dev/null
+++ b/website/docs/api/config.mdx
@@ -0,0 +1,48 @@
+---
+title: Config
+description: Config API endpoints for GitProxy
+---
+
+# Config
+
+*5 endpoints — 5 GET*
+
+## GET `/api/v1/config/attestation`
+
+#### Responses
+
+- 200 Ok
+
+---
+
+## GET `/api/v1/config/contactEmail`
+
+#### Responses
+
+- 200 Ok
+
+---
+
+## GET `/api/v1/config/ssh`
+
+#### Responses
+
+- 200 Ok
+
+---
+
+## GET `/api/v1/config/uiRouteAuth`
+
+#### Responses
+
+- 200 Ok
+
+---
+
+## GET `/api/v1/config/urlShortener`
+
+#### Responses
+
+- 200 Ok
+
+---
diff --git a/website/docs/api/health.mdx b/website/docs/api/health.mdx
new file mode 100644
index 000000000..98c797433
--- /dev/null
+++ b/website/docs/api/health.mdx
@@ -0,0 +1,16 @@
+---
+title: Health
+description: Health API endpoints for GitProxy
+---
+
+# Health
+
+*1 endpoint — 1 GET*
+
+## GET `/api/v1/healthcheck`
+
+#### Responses
+
+- 200 Ok
+
+---
diff --git a/website/docs/api/home.mdx b/website/docs/api/home.mdx
new file mode 100644
index 000000000..098b3a49d
--- /dev/null
+++ b/website/docs/api/home.mdx
@@ -0,0 +1,16 @@
+---
+title: Home
+description: Home API endpoints for GitProxy
+---
+
+# Home
+
+*1 endpoint — 1 GET*
+
+## GET `/api`
+
+#### Responses
+
+- 200 Ok
+
+---
diff --git a/website/docs/api/index.mdx b/website/docs/api/index.mdx
new file mode 100644
index 000000000..4c70037b0
--- /dev/null
+++ b/website/docs/api/index.mdx
@@ -0,0 +1,32 @@
+---
+title: API Reference
+description: REST API reference documentation for GitProxy
+---
+
+# API Reference
+
+Deploy custom push protections and policies on top of Git.
+
+:::info[API Version]
+**2.0.0** — OpenAPI 3.0
+:::
+
+## Authentication
+
+Most endpoints require a **BEARER** token (JWT) passed via the `Authorization` header:
+
+```
+Authorization: Bearer
+```
+
+## Endpoints
+
+35 endpoints across 7 groups:
+
+- [**Auth**](/docs/api/auth) — 6 GET, 4 POST
+- [**Config**](/docs/api/config) — 5 GET
+- [**Health**](/docs/api/health) — 1 GET
+- [**Home**](/docs/api/home) — 1 GET
+- [**Push**](/docs/api/push) — 2 GET, 3 POST
+- [**Repositories**](/docs/api/repositories) — 2 GET, 1 POST, 2 PATCH, 3 DELETE
+- [**Users**](/docs/api/users) — 3 GET, 1 POST, 1 DELETE
diff --git a/website/docs/api/push.mdx b/website/docs/api/push.mdx
new file mode 100644
index 000000000..e5439d111
--- /dev/null
+++ b/website/docs/api/push.mdx
@@ -0,0 +1,126 @@
+---
+title: Push
+description: Push API endpoints for GitProxy
+---
+
+# Push
+
+*5 endpoints — 2 GET, 3 POST*
+
+## GET `/api/v1/push`AUTH
+
+Returns push requests, optionally filtered by query parameters.
+Supported filters: any field from PushQuery (error, blocked, allowPush, authorised, canceled, rejected, type).
+
+:::info[Authorization Required]
+This endpoint requires a valid **JWT Bearer token** in the `Authorization` header.
+:::
+
+#### Responses
+
+- 200 Ok
+
+---
+
+## GET `/api/v1/push/{id}`AUTH
+
+Returns a single push request by ID.
+
+:::info[Authorization Required]
+This endpoint requires a valid **JWT Bearer token** in the `Authorization` header.
+:::
+
+#### Parameters
+
+| Name | In | Type | Required | Description |
+|------|:---:|------|:--------:|-------------|
+| `id` | `path` | `string` | **Yes** | |
+
+#### Responses
+
+- 200 Ok
+- 404 Not Found
+
+---
+
+## POST `/api/v1/push/{id}/authorise`AUTH
+
+Authorises (approves) a pending push request.
+
+:::info[Authorization Required]
+This endpoint requires a valid **JWT Bearer token** in the `Authorization` header.
+:::
+
+#### Parameters
+
+| Name | In | Type | Required | Description |
+|------|:---:|------|:--------:|-------------|
+| `id` | `path` | `string` | **Yes** | |
+
+#### Request Body
+
+| Field | Type | Required | Description |
+|-------|------|:--------:|-------------|
+| `params` | `object` | **Yes** | |
+
+#### Responses
+
+- 200 Ok
+- 400 Bad Request
+- 401 Unauthorized
+- 403 Forbidden
+- 404 Not Found
+
+---
+
+## POST `/api/v1/push/{id}/cancel`AUTH
+
+Cancels a pending push request.
+
+:::info[Authorization Required]
+This endpoint requires a valid **JWT Bearer token** in the `Authorization` header.
+:::
+
+#### Parameters
+
+| Name | In | Type | Required | Description |
+|------|:---:|------|:--------:|-------------|
+| `id` | `path` | `string` | **Yes** | |
+
+#### Responses
+
+- 200 Ok
+- 401 Unauthorized
+- 403 Forbidden
+
+---
+
+## POST `/api/v1/push/{id}/reject`AUTH
+
+Rejects a pending push request.
+
+:::info[Authorization Required]
+This endpoint requires a valid **JWT Bearer token** in the `Authorization` header.
+:::
+
+#### Parameters
+
+| Name | In | Type | Required | Description |
+|------|:---:|------|:--------:|-------------|
+| `id` | `path` | `string` | **Yes** | |
+
+#### Request Body
+
+| Field | Type | Required | Description |
+|-------|------|:--------:|-------------|
+| `reason` | `string` | **Yes** | The reason for rejecting the push request. |
+
+#### Responses
+
+- 200 Ok
+- 400 Bad Request
+- 401 Unauthorized
+- 403 Forbidden
+- 404 Not Found
+
+---
diff --git a/website/docs/api/repositories.mdx b/website/docs/api/repositories.mdx
new file mode 100644
index 000000000..b9e9515e6
--- /dev/null
+++ b/website/docs/api/repositories.mdx
@@ -0,0 +1,194 @@
+---
+title: Repositories
+description: Repositories API endpoints for GitProxy
+---
+
+# Repositories
+
+*8 endpoints — 2 GET, 1 POST, 2 PATCH, 3 DELETE*
+
+## GET `/api/v1/repo`AUTH
+
+Returns repositories, optionally filtered by query parameters.
+
+:::info[Authorization Required]
+This endpoint requires a valid **JWT Bearer token** in the `Authorization` header.
+:::
+
+#### Responses
+
+- 200 Ok
+
+---
+
+## GET `/api/v1/repo/{id}`AUTH
+
+Returns a single repository by ID.
+
+:::info[Authorization Required]
+This endpoint requires a valid **JWT Bearer token** in the `Authorization` header.
+:::
+
+#### Parameters
+
+| Name | In | Type | Required | Description |
+|------|:---:|------|:--------:|-------------|
+| `id` | `path` | `string` | **Yes** | |
+
+#### Responses
+
+- 200 Ok
+- 404 Not Found
+
+---
+
+## POST `/api/v1/repo`AUTH
+
+Creates a new repository. May restart the proxy if a new origin is added.
+
+:::info[Authorization Required]
+This endpoint requires a valid **JWT Bearer token** in the `Authorization` header.
+:::
+
+#### Request Body
+
+| Field | Type | Required | Description |
+|-------|------|:--------:|-------------|
+| `url` | `string` | **Yes** | |
+| `name` | `string` | **Yes** | |
+| `project` | `string` | **Yes** | |
+| `users` | `object` | No | |
+| `_id` | `string` | No | |
+
+#### Responses
+
+- 200 Ok
+- 400 Bad Request
+- 401 Unauthorized
+- 409 Conflict
+- 500 Internal Server Error
+
+---
+
+## PATCH `/api/v1/repo/{id}/user/authorise`AUTH
+
+Grants a user authorise permission on a repository.
+
+:::info[Authorization Required]
+This endpoint requires a valid **JWT Bearer token** in the `Authorization` header.
+:::
+
+#### Parameters
+
+| Name | In | Type | Required | Description |
+|------|:---:|------|:--------:|-------------|
+| `id` | `path` | `string` | **Yes** | |
+
+#### Request Body
+
+| Field | Type | Required | Description |
+|-------|------|:--------:|-------------|
+| `username` | `string` | **Yes** | |
+
+#### Responses
+
+- 200 Ok
+- 400 Bad Request
+- 401 Unauthorized
+
+---
+
+## PATCH `/api/v1/repo/{id}/user/push`AUTH
+
+Grants a user push permission on a repository.
+
+:::info[Authorization Required]
+This endpoint requires a valid **JWT Bearer token** in the `Authorization` header.
+:::
+
+#### Parameters
+
+| Name | In | Type | Required | Description |
+|------|:---:|------|:--------:|-------------|
+| `id` | `path` | `string` | **Yes** | |
+
+#### Request Body
+
+| Field | Type | Required | Description |
+|-------|------|:--------:|-------------|
+| `username` | `string` | **Yes** | |
+
+#### Responses
+
+- 200 Ok
+- 400 Bad Request
+- 401 Unauthorized
+
+---
+
+## DELETE `/api/v1/repo/{id}/delete`AUTH
+
+Deletes a repository. May restart the proxy if a proxied host is removed.
+
+:::info[Authorization Required]
+This endpoint requires a valid **JWT Bearer token** in the `Authorization` header.
+:::
+
+#### Parameters
+
+| Name | In | Type | Required | Description |
+|------|:---:|------|:--------:|-------------|
+| `id` | `path` | `string` | **Yes** | |
+
+#### Responses
+
+- 200 Ok
+- 401 Unauthorized
+
+---
+
+## DELETE `/api/v1/repo/{id}/user/authorise/{username}`AUTH
+
+Revokes a user's authorise permission on a repository.
+
+:::info[Authorization Required]
+This endpoint requires a valid **JWT Bearer token** in the `Authorization` header.
+:::
+
+#### Parameters
+
+| Name | In | Type | Required | Description |
+|------|:---:|------|:--------:|-------------|
+| `id` | `path` | `string` | **Yes** | |
+| `username` | `path` | `string` | **Yes** | |
+
+#### Responses
+
+- 200 Ok
+- 400 Bad Request
+- 401 Unauthorized
+
+---
+
+## DELETE `/api/v1/repo/{id}/user/push/{username}`AUTH
+
+Revokes a user's push permission on a repository.
+
+:::info[Authorization Required]
+This endpoint requires a valid **JWT Bearer token** in the `Authorization` header.
+:::
+
+#### Parameters
+
+| Name | In | Type | Required | Description |
+|------|:---:|------|:--------:|-------------|
+| `id` | `path` | `string` | **Yes** | |
+| `username` | `path` | `string` | **Yes** | |
+
+#### Responses
+
+- 200 Ok
+- 400 Bad Request
+- 401 Unauthorized
+
+---
diff --git a/website/docs/api/users.mdx b/website/docs/api/users.mdx
new file mode 100644
index 000000000..c35947709
--- /dev/null
+++ b/website/docs/api/users.mdx
@@ -0,0 +1,127 @@
+---
+title: Users
+description: Users API endpoints for GitProxy
+---
+
+# Users
+
+*5 endpoints — 3 GET, 1 POST, 1 DELETE*
+
+## GET `/api/v1/user`AUTH
+
+Returns all registered users (public fields only).
+
+:::info[Authorization Required]
+This endpoint requires a valid **JWT Bearer token** in the `Authorization` header.
+:::
+
+#### Responses
+
+- 200 Ok
+
+---
+
+## GET `/api/v1/user/{id}`AUTH
+
+Returns a single user by username.
+
+:::info[Authorization Required]
+This endpoint requires a valid **JWT Bearer token** in the `Authorization` header.
+:::
+
+#### Parameters
+
+| Name | In | Type | Required | Description |
+|------|:---:|------|:--------:|-------------|
+| `id` | `path` | `string` | **Yes** | |
+
+#### Responses
+
+- 200 Ok
+- 404 Not Found
+
+---
+
+## GET `/api/v1/user/{username}/ssh-key-fingerprints`AUTH
+
+Returns the SSH key fingerprints for a user.
+Users may view their own keys; admins may view any user's keys.
+
+:::info[Authorization Required]
+This endpoint requires a valid **JWT Bearer token** in the `Authorization` header.
+:::
+
+#### Parameters
+
+| Name | In | Type | Required | Description |
+|------|:---:|------|:--------:|-------------|
+| `username` | `path` | `string` | **Yes** | |
+
+#### Responses
+
+- 200 Ok
+- 401 Unauthorized
+- 403 Forbidden
+- 500 Internal Server Error
+
+---
+
+## POST `/api/v1/user/{username}/ssh-keys`AUTH
+
+Adds an SSH public key to a user's account.
+Users may add keys to their own account; admins may add to any account.
+
+:::info[Authorization Required]
+This endpoint requires a valid **JWT Bearer token** in the `Authorization` header.
+:::
+
+#### Parameters
+
+| Name | In | Type | Required | Description |
+|------|:---:|------|:--------:|-------------|
+| `username` | `path` | `string` | **Yes** | |
+
+#### Request Body
+
+| Field | Type | Required | Description |
+|-------|------|:--------:|-------------|
+| `publicKey` | `string` | No | |
+| `name` | `string` | No | |
+
+#### Responses
+
+- 200 Ok
+- 400 Bad Request
+- 401 Unauthorized
+- 403 Forbidden
+- 404 Not Found
+- 409 Conflict
+- 500 Internal Server Error
+
+---
+
+## DELETE `/api/v1/user/{username}/ssh-keys/{fingerprint}`AUTH
+
+Removes an SSH public key from a user's account by fingerprint.
+Users may remove keys from their own account; admins may remove from any account.
+
+:::info[Authorization Required]
+This endpoint requires a valid **JWT Bearer token** in the `Authorization` header.
+:::
+
+#### Parameters
+
+| Name | In | Type | Required | Description |
+|------|:---:|------|:--------:|-------------|
+| `username` | `path` | `string` | **Yes** | |
+| `fingerprint` | `path` | `string` | **Yes** | |
+
+#### Responses
+
+- 200 Ok
+- 401 Unauthorized
+- 403 Forbidden
+- 404 Not Found
+- 500 Internal Server Error
+
+---
diff --git a/website/docs/architecture/architecture.md b/website/docs/architecture/architecture.md
index 97da3ce38..7e70974f8 100644
--- a/website/docs/architecture/architecture.md
+++ b/website/docs/architecture/architecture.md
@@ -452,7 +452,7 @@ Allows defining ways to authenticate to the API. This is useful for securing cus
If `apiAuthentication` is left empty, API endpoints will be publicly accesible.
-Currently, only JWT auth is supported. This is implemented via the [`jwtAuthHandler` middleware](https://github.com/finos/git-proxy/blob/main/src/service/passport/jwtAuthHandler.ts). Aside of validating incoming access tokens, it can also assign roles based on the token payload.
+Currently, only JWT auth is supported. This is implemented via the [`@Security('jwt')` decorator](https://github.com/finos/git-proxy/blob/main/src/service/authentication.ts). Aside of validating incoming access tokens, it can also assign roles based on the token payload.
##### Setting up JWT Authentication
diff --git a/website/sidebars.js b/website/sidebars.js
index 9a20aa037..de1b91238 100644
--- a/website/sidebars.js
+++ b/website/sidebars.js
@@ -52,6 +52,25 @@ module.exports = {
collapsed: false,
items: ['configuration/overview', 'configuration/reference', 'configuration/pre-receive'],
},
+ {
+ type: 'category',
+ label: 'API Reference',
+ link: {
+ type: 'doc',
+ id: 'api/index',
+ },
+ collapsible: true,
+ collapsed: false,
+ items: [
+ 'api/auth',
+ 'api/config',
+ 'api/health',
+ 'api/home',
+ 'api/push',
+ 'api/repositories',
+ 'api/users',
+ ],
+ },
{
type: 'category',
label: 'Architecture',
@@ -70,6 +89,7 @@ module.exports = {
'architecture/ssh-architecture',
],
},
+ 'deployment',
{
type: 'category',
label: 'Development',