diff --git a/package-lock.json b/package-lock.json index 0d37d74..7d8d61b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "@opentelemetry/sdk-trace-node": "^2.8.0", "@opentelemetry/semantic-conventions": "^1.41.1", "@prisma/client": "^7.8.0", + "@prisma/config": "^7.10.0", "@types/react": "^19.2.17", "axios": "^1.17.0", "class-transformer": "^0.5.1", @@ -3207,6 +3208,7 @@ "version": "7.8.0", "resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.8.0.tgz", "integrity": "sha512-HFp3Dawv/3sU3JtlPha90IB+48lS7zHiH4LKZPjmcE8YH5P9DOXGPvo8dqOtO7MqLDd1p2hOWMcFlRT1DMblHw==", + "license": "Apache-2.0", "dependencies": { "@prisma/client-runtime-utils": "7.8.0" }, @@ -3232,10 +3234,10 @@ "integrity": "sha512-5NQZztQ0oY/ADFkmd9gPuweH5A1/CCY8YQPorLLO0Mu6a87mY5gsnDkzmFmIHs9NFaLnZojzgddFVN4RpKYrdw==" }, "node_modules/@prisma/config": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.8.0.tgz", - "integrity": "sha512-HFESzd9rx2ZQxlK+TL7tu1HPvCqrHiL6LCxYykI2c34mvaUuIVVl3lYuicJD/MNnzgPnyeBEMlK4WTomJCV5jw==", - "devOptional": true, + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.10.0.tgz", + "integrity": "sha512-Rcg828gIRE3HOQ3pOATFjV5d/P0U9OIobxhd/IMxlfWjA4vru0eGwb0AIwFw0rmcLMVShohZYWPixVxkBHsxUA==", + "license": "Apache-2.0", "dependencies": { "c12": "3.3.4", "deepmerge-ts": "7.1.5", @@ -3616,8 +3618,7 @@ "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "devOptional": true + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==" }, "node_modules/@swc/helpers": { "version": "0.5.23", @@ -5174,7 +5175,6 @@ "version": "3.3.4", "resolved": "https://registry.npmjs.org/c12/-/c12-3.3.4.tgz", "integrity": "sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==", - "devOptional": true, "dependencies": { "chokidar": "^5.0.0", "confbox": "^0.2.4", @@ -5344,7 +5344,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", - "devOptional": true, "dependencies": { "readdirp": "^5.0.0" }, @@ -5549,8 +5548,7 @@ "node_modules/confbox": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", - "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", - "devOptional": true + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==" }, "node_modules/content-disposition": { "version": "1.1.0", @@ -5879,7 +5877,6 @@ "version": "7.1.5", "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", - "devOptional": true, "engines": { "node": ">=16.0.0" } @@ -5887,8 +5884,7 @@ "node_modules/defu": { "version": "6.1.7", "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", - "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", - "devOptional": true + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==" }, "node_modules/delayed-stream": { "version": "1.0.0", @@ -5929,8 +5925,7 @@ "node_modules/destr": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", - "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", - "devOptional": true + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==" }, "node_modules/detect-newline": { "version": "3.1.0", @@ -5999,7 +5994,6 @@ "version": "17.4.2", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", - "devOptional": true, "engines": { "node": ">=12" }, @@ -6046,7 +6040,6 @@ "version": "3.20.0", "resolved": "https://registry.npmjs.org/effect/-/effect-3.20.0.tgz", "integrity": "sha512-qMLfDJscrNG8p/aw+IkT9W7fgj50Z4wG5bLBy0Txsxz8iUHjDIkOgO3SV0WZfnQbNG2VJYb0b+rDLMrhM4+Krw==", - "devOptional": true, "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" @@ -6081,7 +6074,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", - "devOptional": true, "engines": { "node": ">=14" } @@ -6692,14 +6684,12 @@ "node_modules/exsolve": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", - "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", - "devOptional": true + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==" }, "node_modules/fast-check": { "version": "3.23.2", "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", - "devOptional": true, "funding": [ { "type": "individual", @@ -7160,7 +7150,6 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/giget/-/giget-3.3.0.tgz", "integrity": "sha512-gzi2D96p+AMfDcmJHGDj3KJ9NRiwvlFAU5yfa3ROwWZmFUjX4P43x3BcyRaOMMLto1vUo7C+86+MFhYTl6Ryiw==", - "devOptional": true, "bin": { "giget": "dist/cli.mjs" } @@ -9080,7 +9069,6 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", - "devOptional": true, "bin": { "jiti": "lib/jiti-cli.mjs" } @@ -10280,8 +10268,7 @@ "node_modules/ohash": { "version": "2.0.11", "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", - "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", - "devOptional": true + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==" }, "node_modules/on-finished": { "version": "2.4.1", @@ -10565,8 +10552,7 @@ "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "devOptional": true + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==" }, "node_modules/pause": { "version": "0.0.1", @@ -10602,8 +10588,7 @@ "node_modules/perfect-debounce": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", - "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", - "devOptional": true + "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==" }, "node_modules/pg": { "version": "8.21.0", @@ -10800,7 +10785,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz", "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==", - "devOptional": true, "dependencies": { "confbox": "^0.2.4", "exsolve": "^1.0.8", @@ -10949,6 +10933,7 @@ "integrity": "sha512-yfN4yrw7HV9kEJhoy1+jgah0jafEIQsf7uWouSsM8MvJtlubsk+kM7AIBWZ8+GJl74Yj3c+nbYqBkMOxtsZ3Lw==", "devOptional": true, "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { "@prisma/config": "7.8.0", "@prisma/dev": "0.24.3", @@ -10976,6 +10961,19 @@ } } }, + "node_modules/prisma/node_modules/@prisma/config": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.8.0.tgz", + "integrity": "sha512-HFESzd9rx2ZQxlK+TL7tu1HPvCqrHiL6LCxYykI2c34mvaUuIVVl3lYuicJD/MNnzgPnyeBEMlK4WTomJCV5jw==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "c12": "3.3.4", + "deepmerge-ts": "7.1.5", + "effect": "3.20.0", + "empathic": "2.0.0" + } + }, "node_modules/proper-lockfile": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", @@ -11051,7 +11049,6 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", - "devOptional": true, "funding": [ { "type": "individual", @@ -11142,7 +11139,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/rc9/-/rc9-3.0.1.tgz", "integrity": "sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ==", - "devOptional": true, "dependencies": { "defu": "^6.1.6", "destr": "^2.0.5" @@ -11351,7 +11347,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", - "devOptional": true, "engines": { "node": ">= 20.19.0" }, @@ -15470,10 +15465,9 @@ "integrity": "sha512-5NQZztQ0oY/ADFkmd9gPuweH5A1/CCY8YQPorLLO0Mu6a87mY5gsnDkzmFmIHs9NFaLnZojzgddFVN4RpKYrdw==" }, "@prisma/config": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.8.0.tgz", - "integrity": "sha512-HFESzd9rx2ZQxlK+TL7tu1HPvCqrHiL6LCxYykI2c34mvaUuIVVl3lYuicJD/MNnzgPnyeBEMlK4WTomJCV5jw==", - "devOptional": true, + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.10.0.tgz", + "integrity": "sha512-Rcg828gIRE3HOQ3pOATFjV5d/P0U9OIobxhd/IMxlfWjA4vru0eGwb0AIwFw0rmcLMVShohZYWPixVxkBHsxUA==", "requires": { "c12": "3.3.4", "deepmerge-ts": "7.1.5", @@ -15761,8 +15755,7 @@ "@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "devOptional": true + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==" }, "@swc/helpers": { "version": "0.5.23", @@ -16792,7 +16785,6 @@ "version": "3.3.4", "resolved": "https://registry.npmjs.org/c12/-/c12-3.3.4.tgz", "integrity": "sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==", - "devOptional": true, "requires": { "chokidar": "^5.0.0", "confbox": "^0.2.4", @@ -16895,7 +16887,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", - "devOptional": true, "requires": { "readdirp": "^5.0.0" } @@ -17035,8 +17026,7 @@ "confbox": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", - "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", - "devOptional": true + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==" }, "content-disposition": { "version": "1.1.0", @@ -17244,14 +17234,12 @@ "deepmerge-ts": { "version": "7.1.5", "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", - "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", - "devOptional": true + "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==" }, "defu": { "version": "6.1.7", "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", - "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", - "devOptional": true + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==" }, "delayed-stream": { "version": "1.0.0", @@ -17278,8 +17266,7 @@ "destr": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", - "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", - "devOptional": true + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==" }, "detect-newline": { "version": "3.1.0", @@ -17329,8 +17316,7 @@ "dotenv": { "version": "17.4.2", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", - "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", - "devOptional": true + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==" }, "dunder-proto": { "version": "1.0.1", @@ -17365,7 +17351,6 @@ "version": "3.20.0", "resolved": "https://registry.npmjs.org/effect/-/effect-3.20.0.tgz", "integrity": "sha512-qMLfDJscrNG8p/aw+IkT9W7fgj50Z4wG5bLBy0Txsxz8iUHjDIkOgO3SV0WZfnQbNG2VJYb0b+rDLMrhM4+Krw==", - "devOptional": true, "requires": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" @@ -17391,8 +17376,7 @@ "empathic": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", - "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", - "devOptional": true + "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==" }, "encodeurl": { "version": "2.0.0", @@ -17799,14 +17783,12 @@ "exsolve": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", - "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", - "devOptional": true + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==" }, "fast-check": { "version": "3.23.2", "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", - "devOptional": true, "requires": { "pure-rand": "^6.1.0" } @@ -18110,8 +18092,7 @@ "giget": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/giget/-/giget-3.3.0.tgz", - "integrity": "sha512-gzi2D96p+AMfDcmJHGDj3KJ9NRiwvlFAU5yfa3ROwWZmFUjX4P43x3BcyRaOMMLto1vUo7C+86+MFhYTl6Ryiw==", - "devOptional": true + "integrity": "sha512-gzi2D96p+AMfDcmJHGDj3KJ9NRiwvlFAU5yfa3ROwWZmFUjX4P43x3BcyRaOMMLto1vUo7C+86+MFhYTl6Ryiw==" }, "git-raw-commits": { "version": "2.0.11", @@ -19417,8 +19398,7 @@ "jiti": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", - "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", - "devOptional": true + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==" }, "js-md5": { "version": "0.8.3", @@ -20211,8 +20191,7 @@ "ohash": { "version": "2.0.11", "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", - "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", - "devOptional": true + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==" }, "on-finished": { "version": "2.4.1", @@ -20403,8 +20382,7 @@ "pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "devOptional": true + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==" }, "pause": { "version": "0.0.1", @@ -20434,8 +20412,7 @@ "perfect-debounce": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", - "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", - "devOptional": true + "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==" }, "pg": { "version": "8.21.0", @@ -20572,7 +20549,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz", "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==", - "devOptional": true, "requires": { "confbox": "^0.2.4", "exsolve": "^1.0.8", @@ -20677,6 +20653,20 @@ "@prisma/studio-core": "0.27.3", "mysql2": "3.15.3", "postgres": "3.4.7" + }, + "dependencies": { + "@prisma/config": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.8.0.tgz", + "integrity": "sha512-HFESzd9rx2ZQxlK+TL7tu1HPvCqrHiL6LCxYykI2c34mvaUuIVVl3lYuicJD/MNnzgPnyeBEMlK4WTomJCV5jw==", + "devOptional": true, + "requires": { + "c12": "3.3.4", + "deepmerge-ts": "7.1.5", + "effect": "3.20.0", + "empathic": "2.0.0" + } + } } }, "proper-lockfile": { @@ -20739,8 +20729,7 @@ "pure-rand": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", - "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", - "devOptional": true + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==" }, "qs": { "version": "6.15.3", @@ -20783,7 +20772,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/rc9/-/rc9-3.0.1.tgz", "integrity": "sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ==", - "devOptional": true, "requires": { "defu": "^6.1.6", "destr": "^2.0.5" @@ -20936,8 +20924,7 @@ "readdirp": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", - "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", - "devOptional": true + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==" }, "redent": { "version": "3.0.0", diff --git a/package.json b/package.json index 6ff6a6d..ba42de6 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "@opentelemetry/sdk-trace-node": "^2.8.0", "@opentelemetry/semantic-conventions": "^1.41.1", "@prisma/client": "^7.8.0", + "@prisma/config": "^7.10.0", "@types/react": "^19.2.17", "axios": "^1.17.0", "class-transformer": "^0.5.1", diff --git a/prisma/migrations/20260825181000_add_forensic_case_management/migration.sql b/prisma/migrations/20260825181000_add_forensic_case_management/migration.sql new file mode 100644 index 0000000..6782a1f --- /dev/null +++ b/prisma/migrations/20260825181000_add_forensic_case_management/migration.sql @@ -0,0 +1,116 @@ +-- CreateEnum +CREATE TYPE "CaseStatus" AS ENUM ('DRAFT', 'ACTIVE', 'IN_REVIEW', 'SUSPENDED', 'CLOSED', 'ARCHIVED'); + +-- CreateEnum +CREATE TYPE "CaseSeverity" AS ENUM ('LOW', 'MEDIUM', 'HIGH', 'CRITICAL'); + +-- CreateEnum +CREATE TYPE "CaseAssigneeRole" AS ENUM ('INVESTIGATOR', 'LEGAL_OBSERVER', 'EXTERNAL_AUDITOR'); + +-- CreateTable +CREATE TABLE "cases" ( + "id" TEXT NOT NULL, + "caseNumber" TEXT NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT NOT NULL, + "status" "CaseStatus" NOT NULL DEFAULT 'DRAFT', + "severity" "CaseSeverity" NOT NULL DEFAULT 'MEDIUM', + "leadInvestigatorId" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "closedAt" TIMESTAMP(3), + "nextReviewAt" TIMESTAMP(3), + + CONSTRAINT "cases_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "case_assignees" ( + "id" TEXT NOT NULL, + "caseId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "role" "CaseAssigneeRole" NOT NULL DEFAULT 'INVESTIGATOR', + "assignedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "case_assignees_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "case_incidents" ( + "id" TEXT NOT NULL, + "caseId" TEXT NOT NULL, + "incidentId" TEXT NOT NULL, + "linkedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "case_incidents_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "case_activity_logs" ( + "id" TEXT NOT NULL, + "caseId" TEXT NOT NULL, + "actorId" TEXT NOT NULL, + "action" TEXT NOT NULL, + "ipAddress" TEXT, + "changedFields" JSONB, + "timestamp" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "case_activity_logs_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "cases_caseNumber_key" ON "cases"("caseNumber"); + +-- CreateIndex +CREATE INDEX "cases_status_idx" ON "cases"("status"); + +-- CreateIndex +CREATE INDEX "cases_severity_idx" ON "cases"("severity"); + +-- CreateIndex +CREATE INDEX "cases_leadInvestigatorId_idx" ON "cases"("leadInvestigatorId"); + +-- CreateIndex +CREATE INDEX "cases_caseNumber_idx" ON "cases"("caseNumber"); + +-- CreateIndex +CREATE UNIQUE INDEX "case_assignees_caseId_userId_key" ON "case_assignees"("caseId", "userId"); + +-- CreateIndex +CREATE INDEX "case_assignees_userId_idx" ON "case_assignees"("userId"); + +-- CreateIndex +CREATE UNIQUE INDEX "case_incidents_caseId_incidentId_key" ON "case_incidents"("caseId", "incidentId"); + +-- CreateIndex +CREATE INDEX "case_incidents_incidentId_idx" ON "case_incidents"("incidentId"); + +-- CreateIndex +CREATE INDEX "case_activity_logs_caseId_idx" ON "case_activity_logs"("caseId"); + +-- CreateIndex +CREATE INDEX "case_activity_logs_actorId_idx" ON "case_activity_logs"("actorId"); + +-- AddForeignKey +ALTER TABLE "cases" ADD CONSTRAINT "cases_leadInvestigatorId_fkey" FOREIGN KEY ("leadInvestigatorId") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "case_assignees" ADD CONSTRAINT "case_assignees_caseId_fkey" FOREIGN KEY ("caseId") REFERENCES "cases"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "case_assignees" ADD CONSTRAINT "case_assignees_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "case_incidents" ADD CONSTRAINT "case_incidents_caseId_fkey" FOREIGN KEY ("caseId") REFERENCES "cases"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "case_incidents" ADD CONSTRAINT "case_incidents_incidentId_fkey" FOREIGN KEY ("incidentId") REFERENCES "incidents"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "case_activity_logs" ADD CONSTRAINT "case_activity_logs_caseId_fkey" FOREIGN KEY ("caseId") REFERENCES "cases"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "case_activity_logs" ADD CONSTRAINT "case_activity_logs_actorId_fkey" FOREIGN KEY ("actorId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "investigation_notes" ADD CONSTRAINT "investigation_notes_caseId_fkey" FOREIGN KEY ("caseId") REFERENCES "cases"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 29ce9ab..5040033 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -19,6 +19,9 @@ model User { memberships Membership[] invitationsSent Invitation[] ownedOrganizations Organization[] @relation("OrganizationOwner") + leadCases Case[] @relation("LeadInvestigator") + caseAssignments CaseAssignee[] + caseActivities CaseActivityLog[] @relation("CaseActivityActor") @@map("users") } @@ -163,6 +166,7 @@ model InvestigationNote { updatedAt DateTime @updatedAt auditEntries NoteAuditLog[] + case Case? @relation(fields: [caseId], references: [id], onDelete: Cascade) @@index([caseId]) @@map("investigation_notes") @@ -211,6 +215,7 @@ model Incident { assignedUser User? @relation(fields: [assignedUserId], references: [id]) alerts Alert[] auditEntries IncidentAuditLog[] + caseIncidents CaseIncident[] @@index([status]) @@index([severity]) @@ -236,6 +241,104 @@ model IncidentAuditLog { @@map("incident_audit_logs") } +// --------------------------------------------------------------------------- +// Forensic Investigation & Case Management – issue #126 +// --------------------------------------------------------------------------- + +enum CaseStatus { + DRAFT + ACTIVE + IN_REVIEW + SUSPENDED + CLOSED + ARCHIVED +} + +enum CaseSeverity { + LOW + MEDIUM + HIGH + CRITICAL +} + +enum CaseAssigneeRole { + INVESTIGATOR + LEGAL_OBSERVER + EXTERNAL_AUDITOR +} + +model Case { + id String @id @default(uuid()) + caseNumber String @unique + title String + description String + status CaseStatus @default(DRAFT) + severity CaseSeverity @default(MEDIUM) + leadInvestigatorId String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + closedAt DateTime? + nextReviewAt DateTime? + + leadInvestigator User? @relation("LeadInvestigator", fields: [leadInvestigatorId], references: [id], onDelete: SetNull) + assignees CaseAssignee[] + incidents CaseIncident[] + activityLogs CaseActivityLog[] + notes InvestigationNote[] + + @@index([status]) + @@index([severity]) + @@index([leadInvestigatorId]) + @@index([caseNumber]) + @@map("cases") +} + +model CaseAssignee { + id String @id @default(uuid()) + caseId String + userId String + role CaseAssigneeRole @default(INVESTIGATOR) + assignedAt DateTime @default(now()) + + case Case @relation(fields: [caseId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id], onDelete: Restrict) + + @@unique([caseId, userId]) + @@index([userId]) + @@map("case_assignees") +} + +model CaseIncident { + id String @id @default(uuid()) + caseId String + incidentId String + linkedAt DateTime @default(now()) + + case Case @relation(fields: [caseId], references: [id], onDelete: Restrict) + incident Incident @relation(fields: [incidentId], references: [id], onDelete: Restrict) + + @@unique([caseId, incidentId]) + @@index([incidentId]) + @@map("case_incidents") +} + +model CaseActivityLog { + id String @id @default(uuid()) + caseId String + actorId String + action String + ipAddress String? + changedFields Json? + timestamp DateTime @default(now()) + + case Case @relation(fields: [caseId], references: [id], onDelete: Restrict) + actor User @relation("CaseActivityActor", fields: [actorId], references: [id], onDelete: Restrict) + + @@index([caseId]) + @@index([actorId]) + @@map("case_activity_logs") +} + // --------------------------------------------------------------------------- // Protocol Health Monitoring // --------------------------------------------------------------------------- diff --git a/src/domain/cases/types/index.ts b/src/domain/cases/types/index.ts new file mode 100644 index 0000000..de3a88a --- /dev/null +++ b/src/domain/cases/types/index.ts @@ -0,0 +1,132 @@ +/** + * Forensic Investigation & Case Management Domain Types (#126) + */ + +export enum CaseStatus { + DRAFT = 'DRAFT', + ACTIVE = 'ACTIVE', + IN_REVIEW = 'IN_REVIEW', + SUSPENDED = 'SUSPENDED', + CLOSED = 'CLOSED', + ARCHIVED = 'ARCHIVED', +} + +export enum CaseSeverity { + LOW = 'LOW', + MEDIUM = 'MEDIUM', + HIGH = 'HIGH', + CRITICAL = 'CRITICAL', +} + +export enum CaseAssigneeRole { + INVESTIGATOR = 'INVESTIGATOR', + LEGAL_OBSERVER = 'LEGAL_OBSERVER', + EXTERNAL_AUDITOR = 'EXTERNAL_AUDITOR', +} + +export interface CaseUserSummary { + id: string; + email: string; +} + +export interface CaseIncidentSummary { + id: string; + title: string; + severity: string; + status: string; +} + +export interface CaseAssignee { + id: string; + caseId: string; + userId: string; + role: CaseAssigneeRole; + assignedAt: Date; + user?: CaseUserSummary; +} + +export interface CaseIncident { + id: string; + caseId: string; + incidentId: string; + linkedAt: Date; + incident?: CaseIncidentSummary; +} + +export interface CaseActivityLog { + id: string; + caseId: string; + actorId: string; + action: string; + ipAddress?: string | null; + changedFields?: Record | null; + timestamp: Date; + actor?: CaseUserSummary; +} + +export interface Case { + id: string; + caseNumber: string; + title: string; + description: string; + status: CaseStatus; + severity: CaseSeverity; + leadInvestigatorId?: string | null; + createdAt: Date; + updatedAt: Date; + closedAt?: Date | null; + nextReviewAt?: Date | null; + leadInvestigator?: CaseUserSummary | null; + assignees?: CaseAssignee[]; + incidents?: CaseIncident[]; + activityLogs?: CaseActivityLog[]; +} + +export interface CreateCaseInput { + caseNumber?: string; + title: string; + description: string; + status?: CaseStatus; + severity?: CaseSeverity; + leadInvestigatorId?: string | null; + nextReviewAt?: Date | null; + assigneeUserIds?: Array<{ userId: string; role?: CaseAssigneeRole }>; + incidentIds?: string[]; + actorId: string; + ipAddress?: string; +} + +export interface UpdateCaseInput { + title?: string; + description?: string; + status?: CaseStatus; + severity?: CaseSeverity; + leadInvestigatorId?: string | null; + nextReviewAt?: Date | null; + closedAt?: Date | null; + actorId: string; + ipAddress?: string; +} + +export interface AddCaseAssigneeInput { + caseId: string; + userId: string; + role: CaseAssigneeRole; + actorId: string; + ipAddress?: string; +} + +export interface LinkCaseIncidentInput { + caseId: string; + incidentId: string; + actorId: string; + ipAddress?: string; +} + +export interface LogCaseActivityInput { + caseId: string; + actorId: string; + action: string; + ipAddress?: string | null; + changedFields?: Record | null; +} diff --git a/src/infrastructure/database/seeds/caseSeed.ts b/src/infrastructure/database/seeds/caseSeed.ts new file mode 100644 index 0000000..e20095c --- /dev/null +++ b/src/infrastructure/database/seeds/caseSeed.ts @@ -0,0 +1,304 @@ +import { PrismaClient, CaseStatus, CaseSeverity, CaseAssigneeRole } from '@prisma/client'; + +/** + * Case Management Seed Script (#126) + * + * Populates realistic mock forensic cases, multi-incident attachments, + * multi-role assignees, and audit history logs for local integration testing. + */ +export async function seedCases(prisma: PrismaClient) { + console.log('Seeding Forensic Investigation & Case Management data...'); + + // 1. Seed or retrieve users + const leadInvestigator = await prisma.user.upsert({ + where: { email: 'alice.lead@sentinel.security' }, + update: {}, + create: { + email: 'alice.lead@sentinel.security', + }, + }); + + const forensicAnalyst = await prisma.user.upsert({ + where: { email: 'bob.forensics@sentinel.security' }, + update: {}, + create: { + email: 'bob.forensics@sentinel.security', + }, + }); + + const legalObserver = await prisma.user.upsert({ + where: { email: 'clara.legal@sentinel.security' }, + update: {}, + create: { + email: 'clara.legal@sentinel.security', + }, + }); + + const externalAuditor = await prisma.user.upsert({ + where: { email: 'dan.auditor@external-sec.io' }, + update: {}, + create: { + email: 'dan.auditor@external-sec.io', + }, + }); + + // 2. Seed or retrieve mock incidents + const incidentAlpha = await prisma.incident.upsert({ + where: { id: 'inc-seed-alpha-001' }, + update: {}, + create: { + id: 'inc-seed-alpha-001', + title: 'Suspicious High-Value Flash Loan Drain on Liquidity Pool', + description: 'Anomalous multi-hop swap sequence drained $1.4M from Soroban DEX pair.', + status: 'investigating', + severity: 'critical', + priority: 'p1', + category: 'smart-contract-exploit', + assignedUserId: leadInvestigator.id, + tags: ['flash-loan', 'dex', 'high-priority'], + }, + }); + + const incidentBeta = await prisma.incident.upsert({ + where: { id: 'inc-seed-beta-002' }, + update: {}, + create: { + id: 'inc-seed-beta-002', + title: 'Unauthorized Admin Key Rotation Attempt', + description: 'Multi-sig threshold mutation attempted outside scheduled governance window.', + status: 'contained', + severity: 'high', + priority: 'p2', + category: 'access-control', + assignedUserId: forensicAnalyst.id, + tags: ['multisig', 'governance', 'key-compromise'], + }, + }); + + const incidentGamma = await prisma.incident.upsert({ + where: { id: 'inc-seed-gamma-003' }, + update: {}, + create: { + id: 'inc-seed-gamma-003', + title: 'Dormant Whale Wallet Reactivation & Mixing Interaction', + description: 'Blacklisted address routed 250k XLM through decentralized privacy mixer.', + status: 'open', + severity: 'medium', + priority: 'p3', + category: 'aml-compliance', + assignedUserId: leadInvestigator.id, + tags: ['aml', 'mixer', 'sanctions'], + }, + }); + + // 3. Seed Case 1: Multi-incident Active Critical Case + const case1 = await prisma.case.upsert({ + where: { caseNumber: 'CASE-2026-0891' }, + update: {}, + create: { + caseNumber: 'CASE-2026-0891', + title: 'Operation Horizon: DEX Liquidity Exploit & Associated Key Compromise', + description: + 'Comprehensive forensic investigation tracking attacker root-cause vector across Flash Loan exploit and attempted multi-sig takeover.', + status: CaseStatus.ACTIVE, + severity: CaseSeverity.CRITICAL, + leadInvestigatorId: leadInvestigator.id, + nextReviewAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // +7 days + }, + }); + + // 4. Seed Case 2: In-Review Compliance Case + const case2 = await prisma.case.upsert({ + where: { caseNumber: 'CASE-2026-0892' }, + update: {}, + create: { + caseNumber: 'CASE-2026-0892', + title: 'Sanctioned Entity Fund Tracing & Privacy Mixer Flow Analysis', + description: + 'Regulatory audit case examining outbound transactions to Tornado/privacy mixer endpoints.', + status: CaseStatus.IN_REVIEW, + severity: CaseSeverity.MEDIUM, + leadInvestigatorId: leadInvestigator.id, + nextReviewAt: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000), + }, + }); + + // 5. Seed Case 3: Closed Post-Mortem Case + await prisma.case.upsert({ + where: { caseNumber: 'CASE-2026-0893' }, + update: {}, + create: { + caseNumber: 'CASE-2026-0893', + title: 'Q2 Bridge Relay Protocol Vulnerability Assessment', + description: + 'Historical investigation documenting resolved signature validation bug on cross-chain relay.', + status: CaseStatus.CLOSED, + severity: CaseSeverity.HIGH, + leadInvestigatorId: leadInvestigator.id, + closedAt: new Date(), + }, + }); + + // 6. Assignees for Case 1 (multi-role collaborators) + await prisma.caseAssignee.upsert({ + where: { + caseId_userId: { + caseId: case1.id, + userId: leadInvestigator.id, + }, + }, + update: {}, + create: { + caseId: case1.id, + userId: leadInvestigator.id, + role: CaseAssigneeRole.INVESTIGATOR, + }, + }); + + await prisma.caseAssignee.upsert({ + where: { + caseId_userId: { + caseId: case1.id, + userId: forensicAnalyst.id, + }, + }, + update: {}, + create: { + caseId: case1.id, + userId: forensicAnalyst.id, + role: CaseAssigneeRole.INVESTIGATOR, + }, + }); + + await prisma.caseAssignee.upsert({ + where: { + caseId_userId: { + caseId: case1.id, + userId: legalObserver.id, + }, + }, + update: {}, + create: { + caseId: case1.id, + userId: legalObserver.id, + role: CaseAssigneeRole.LEGAL_OBSERVER, + }, + }); + + await prisma.caseAssignee.upsert({ + where: { + caseId_userId: { + caseId: case1.id, + userId: externalAuditor.id, + }, + }, + update: {}, + create: { + caseId: case1.id, + userId: externalAuditor.id, + role: CaseAssigneeRole.EXTERNAL_AUDITOR, + }, + }); + + // 7. Multi-incident attachments (M:N mapping) + await prisma.caseIncident.upsert({ + where: { + caseId_incidentId: { + caseId: case1.id, + incidentId: incidentAlpha.id, + }, + }, + update: {}, + create: { + caseId: case1.id, + incidentId: incidentAlpha.id, + }, + }); + + await prisma.caseIncident.upsert({ + where: { + caseId_incidentId: { + caseId: case1.id, + incidentId: incidentBeta.id, + }, + }, + update: {}, + create: { + caseId: case1.id, + incidentId: incidentBeta.id, + }, + }); + + await prisma.caseIncident.upsert({ + where: { + caseId_incidentId: { + caseId: case2.id, + incidentId: incidentGamma.id, + }, + }, + update: {}, + create: { + caseId: case2.id, + incidentId: incidentGamma.id, + }, + }); + + // 8. Audit and Activity Ledger with JSON Diffs + await prisma.caseActivityLog.createMany({ + data: [ + { + caseId: case1.id, + actorId: leadInvestigator.id, + action: 'case_created', + ipAddress: '192.168.1.100', + changedFields: { + status: { from: null, to: CaseStatus.DRAFT }, + severity: { from: null, to: CaseSeverity.CRITICAL }, + }, + }, + { + caseId: case1.id, + actorId: leadInvestigator.id, + action: 'status_transition', + ipAddress: '192.168.1.100', + changedFields: { + status: { from: CaseStatus.DRAFT, to: CaseStatus.ACTIVE }, + }, + }, + { + caseId: case1.id, + actorId: leadInvestigator.id, + action: 'incident_linked', + ipAddress: '192.168.1.100', + changedFields: { + incidentId: { from: null, to: incidentAlpha.id }, + }, + }, + { + caseId: case1.id, + actorId: forensicAnalyst.id, + action: 'assignee_added', + ipAddress: '192.168.1.105', + changedFields: { + assigneeUserId: { from: null, to: externalAuditor.id }, + role: { from: null, to: CaseAssigneeRole.EXTERNAL_AUDITOR }, + }, + }, + ], + }); + + console.log('✅ Forensic Investigation & Case Management data seeded successfully.'); +} + +// Direct execution helper +if (require.main === module) { + const prisma = new PrismaClient(); + seedCases(prisma) + .catch(e => { + console.error(e); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); +} diff --git a/tests/domain/cases/case-domain.test.ts b/tests/domain/cases/case-domain.test.ts new file mode 100644 index 0000000..45ff9ed --- /dev/null +++ b/tests/domain/cases/case-domain.test.ts @@ -0,0 +1,155 @@ +import { + CaseStatus, + CaseSeverity, + CaseAssigneeRole, + CreateCaseInput, + UpdateCaseInput, + CaseActivityLog, +} from '../../../src/domain/cases/types'; +import { seedCases } from '../../../src/infrastructure/database/seeds/caseSeed'; +import { PrismaClient } from '@prisma/client'; + +describe('Forensic Investigation & Case Management Domain Model (#126)', () => { + describe('Domain Enums and Types', () => { + it('should properly define all CaseStatus lifecycle enum values', () => { + expect(CaseStatus.DRAFT).toBe('DRAFT'); + expect(CaseStatus.ACTIVE).toBe('ACTIVE'); + expect(CaseStatus.IN_REVIEW).toBe('IN_REVIEW'); + expect(CaseStatus.SUSPENDED).toBe('SUSPENDED'); + expect(CaseStatus.CLOSED).toBe('CLOSED'); + expect(CaseStatus.ARCHIVED).toBe('ARCHIVED'); + }); + + it('should properly define all CaseSeverity enum values', () => { + expect(CaseSeverity.LOW).toBe('LOW'); + expect(CaseSeverity.MEDIUM).toBe('MEDIUM'); + expect(CaseSeverity.HIGH).toBe('HIGH'); + expect(CaseSeverity.CRITICAL).toBe('CRITICAL'); + }); + + it('should properly define all CaseAssigneeRole enum values', () => { + expect(CaseAssigneeRole.INVESTIGATOR).toBe('INVESTIGATOR'); + expect(CaseAssigneeRole.LEGAL_OBSERVER).toBe('LEGAL_OBSERVER'); + expect(CaseAssigneeRole.EXTERNAL_AUDITOR).toBe('EXTERNAL_AUDITOR'); + }); + + it('should structure CreateCaseInput payload correctly', () => { + const input: CreateCaseInput = { + caseNumber: 'CASE-2026-0891', + title: 'Operation Horizon: DEX Liquidity Exploit', + description: 'Forensic analysis of flash-loan exploit', + status: CaseStatus.ACTIVE, + severity: CaseSeverity.CRITICAL, + leadInvestigatorId: 'user-uuid-1', + nextReviewAt: new Date(), + assigneeUserIds: [ + { userId: 'user-uuid-1', role: CaseAssigneeRole.INVESTIGATOR }, + { userId: 'user-uuid-2', role: CaseAssigneeRole.LEGAL_OBSERVER }, + ], + incidentIds: ['incident-uuid-1', 'incident-uuid-2'], + actorId: 'user-uuid-1', + ipAddress: '127.0.0.1', + }; + + expect(input.caseNumber).toBe('CASE-2026-0891'); + expect(input.severity).toBe(CaseSeverity.CRITICAL); + expect(input.assigneeUserIds).toHaveLength(2); + expect(input.incidentIds).toContain('incident-uuid-1'); + }); + + it('should structure UpdateCaseInput with status transitions and audit deltas', () => { + const input: UpdateCaseInput = { + status: CaseStatus.CLOSED, + closedAt: new Date(), + actorId: 'user-uuid-1', + ipAddress: '192.168.1.50', + }; + + expect(input.status).toBe(CaseStatus.CLOSED); + expect(input.closedAt).toBeInstanceOf(Date); + }); + + it('should capture structured JSON diffs in CaseActivityLog', () => { + const log: CaseActivityLog = { + id: 'log-uuid-1', + caseId: 'case-uuid-1', + actorId: 'user-uuid-1', + action: 'status_transition', + ipAddress: '10.0.0.1', + changedFields: { + status: { from: CaseStatus.DRAFT, to: CaseStatus.ACTIVE }, + severity: { from: CaseSeverity.MEDIUM, to: CaseSeverity.HIGH }, + }, + timestamp: new Date(), + }; + + expect(log.action).toBe('status_transition'); + expect(log.changedFields?.status.from).toBe(CaseStatus.DRAFT); + expect(log.changedFields?.status.to).toBe(CaseStatus.ACTIVE); + }); + }); + + describe('Database Seed Script Pipeline', () => { + let mockPrisma: any; + + beforeEach(() => { + mockPrisma = { + user: { + upsert: jest + .fn() + .mockImplementation(({ create }) => Promise.resolve({ id: 'user-id', ...create })), + }, + incident: { + upsert: jest.fn().mockImplementation(({ create }) => Promise.resolve({ ...create })), + }, + case: { + upsert: jest + .fn() + .mockImplementation(({ create }) => Promise.resolve({ id: 'case-id', ...create })), + }, + caseAssignee: { + upsert: jest + .fn() + .mockImplementation(({ create }) => Promise.resolve({ id: 'assignee-id', ...create })), + }, + caseIncident: { + upsert: jest + .fn() + .mockImplementation(({ create }) => Promise.resolve({ id: 'case-inc-id', ...create })), + }, + caseActivityLog: { + createMany: jest.fn().mockResolvedValue({ count: 4 }), + }, + }; + }); + + it('should execute seedCases and create users, cases, assignees, incidents, and activity logs', async () => { + await seedCases(mockPrisma as unknown as PrismaClient); + + // Verify users seeded + expect(mockPrisma.user.upsert).toHaveBeenCalledTimes(4); + + // Verify incidents seeded + expect(mockPrisma.incident.upsert).toHaveBeenCalledTimes(3); + + // Verify cases seeded + expect(mockPrisma.case.upsert).toHaveBeenCalledTimes(3); + + // Verify assignees seeded + expect(mockPrisma.caseAssignee.upsert).toHaveBeenCalledTimes(4); + + // Verify M:N case incidents linked + expect(mockPrisma.caseIncident.upsert).toHaveBeenCalledTimes(3); + + // Verify activity logs with JSON diffs created + expect(mockPrisma.caseActivityLog.createMany).toHaveBeenCalledWith({ + data: expect.arrayContaining([ + expect.objectContaining({ action: 'case_created' }), + expect.objectContaining({ action: 'status_transition' }), + expect.objectContaining({ action: 'incident_linked' }), + expect.objectContaining({ action: 'assignee_added' }), + ]), + }); + }); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index f18e903..ccec881 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,6 +20,13 @@ "@integrations/*": ["./apps/backend/src/integrations/*"] } }, - "include": ["apps/backend/**/*", "libs/**/*", "database/**/*", "prisma.config.ts"], + "include": [ + "apps/backend/**/*", + "libs/**/*", + "database/**/*", + "src/**/*", + "tests/**/*", + "prisma.config.ts" + ], "exclude": ["node_modules", "dist", "apps/dashboard"] }