From 1ede1c7f073088f4c2407313c3e6dc94f9ef81d7 Mon Sep 17 00:00:00 2001 From: Pierre Brisorgueil Date: Mon, 6 Apr 2026 12:09:50 +0200 Subject: [PATCH 01/10] chore(organizations): rename middleware/ to middlewares/ for consistency (#3402) --- modules/billing/routes/billing.routes.js | 2 +- .../organizations.middleware.js | 0 modules/tasks/routes/tasks.routes.js | 2 +- package-lock.json | 251 ++++-------------- 4 files changed, 53 insertions(+), 202 deletions(-) rename modules/organizations/{middleware => middlewares}/organizations.middleware.js (100%) diff --git a/modules/billing/routes/billing.routes.js b/modules/billing/routes/billing.routes.js index bd22375f1..d841f6c40 100644 --- a/modules/billing/routes/billing.routes.js +++ b/modules/billing/routes/billing.routes.js @@ -5,7 +5,7 @@ import passport from 'passport'; import model from '../../../lib/middlewares/model.js'; import policy from '../../../lib/middlewares/policy.js'; -import organization from '../../organizations/middleware/organizations.middleware.js'; +import organization from '../../organizations/middlewares/organizations.middleware.js'; import billingSchema from '../models/billing.subscription.schema.js'; import billingPlans from '../controllers/billing.plans.controller.js'; import billing from '../controllers/billing.controller.js'; diff --git a/modules/organizations/middleware/organizations.middleware.js b/modules/organizations/middlewares/organizations.middleware.js similarity index 100% rename from modules/organizations/middleware/organizations.middleware.js rename to modules/organizations/middlewares/organizations.middleware.js diff --git a/modules/tasks/routes/tasks.routes.js b/modules/tasks/routes/tasks.routes.js index 867abb363..f29b37bd6 100644 --- a/modules/tasks/routes/tasks.routes.js +++ b/modules/tasks/routes/tasks.routes.js @@ -4,7 +4,7 @@ import passport from 'passport'; import model from '../../../lib/middlewares/model.js'; -import organization from '../../organizations/middleware/organizations.middleware.js'; +import organization from '../../organizations/middlewares/organizations.middleware.js'; import policy from '../../../lib/middlewares/policy.js'; import tasks from '../controllers/tasks.controller.js'; import tasksSchema from '../models/tasks.schema.js'; diff --git a/package-lock.json b/package-lock.json index de56b0661..ff42db852 100644 --- a/package-lock.json +++ b/package-lock.json @@ -175,6 +175,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1016,6 +1017,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -1038,6 +1040,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -2260,7 +2263,6 @@ "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.2.0.tgz", "integrity": "sha512-+O1ifRjkvYIkBqASKWgLxrpEhQAAE7hY77ALLUufSk5717KfOShg6IbqLmdsLMPdUiFvA2kTs0R7YZy+l0IzZQ==", "license": "MIT", - "peer": true, "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", @@ -2278,7 +2280,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -2294,7 +2295,6 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -2311,7 +2311,6 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", - "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -2323,15 +2322,13 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@jest/core": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.2.0.tgz", "integrity": "sha512-03W6IhuhjqTlpzh/ojut/pDB2LPRygyWX8ExpgHtQA8H/3K7+1vKmcINx5UzeOX1se6YEsBsOHQ1CRzf3fOwTQ==", "license": "MIT", - "peer": true, "dependencies": { "@jest/console": "30.2.0", "@jest/pattern": "30.0.1", @@ -2379,7 +2376,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -2395,7 +2391,6 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -2412,7 +2407,6 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", - "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -2424,15 +2418,13 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@jest/diff-sequences": { "version": "30.0.1", "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", "license": "MIT", - "peer": true, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } @@ -2442,7 +2434,6 @@ "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.2.0.tgz", "integrity": "sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==", "license": "MIT", - "peer": true, "dependencies": { "@jest/fake-timers": "30.2.0", "@jest/types": "30.2.0", @@ -2682,7 +2673,6 @@ "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.2.0.tgz", "integrity": "sha512-V9yxQK5erfzx99Sf+7LbhBwNWEZ9eZay8qQ9+JSC0TrMR1pMDHLMY+BnVPacWU6Jamrh252/IKo4F1Xn/zfiqA==", "license": "MIT", - "peer": true, "dependencies": { "expect": "30.2.0", "jest-snapshot": "30.2.0" @@ -2696,7 +2686,6 @@ "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.2.0.tgz", "integrity": "sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA==", "license": "MIT", - "peer": true, "dependencies": { "@jest/get-type": "30.1.0" }, @@ -2709,7 +2698,6 @@ "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.2.0.tgz", "integrity": "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==", "license": "MIT", - "peer": true, "dependencies": { "@jest/types": "30.2.0", "@sinonjs/fake-timers": "^13.0.0", @@ -3169,7 +3157,6 @@ "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.2.0.tgz", "integrity": "sha512-DRyW6baWPqKMa9CzeiBjHwjd8XeAyco2Vt8XbcLFjiwCOEKOvy82GJ8QQnJE9ofsxCMPjH4MfH8fCWIHHDKpAQ==", "license": "MIT", - "peer": true, "dependencies": { "@bcoe/v8-coverage": "^0.2.3", "@jest/console": "30.2.0", @@ -3212,7 +3199,6 @@ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "license": "ISC", - "peer": true, "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", @@ -3230,7 +3216,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -3245,15 +3230,13 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@jest/reporters/node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "license": "MIT", - "peer": true, "dependencies": { "balanced-match": "^1.0.0" } @@ -3263,7 +3246,6 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -3280,7 +3262,6 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", - "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -3292,15 +3273,13 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@jest/reporters/node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@jest/reporters/node_modules/glob": { "version": "10.5.0", @@ -3308,7 +3287,6 @@ "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "license": "ISC", - "peer": true, "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", @@ -3329,7 +3307,6 @@ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "license": "BlueOak-1.0.0", - "peer": true, "dependencies": { "@isaacs/cliui": "^8.0.2" }, @@ -3344,15 +3321,13 @@ "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/@jest/reporters/node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "license": "ISC", - "peer": true, "dependencies": { "brace-expansion": "^2.0.1" }, @@ -3368,7 +3343,6 @@ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "license": "BlueOak-1.0.0", - "peer": true, "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" @@ -3385,7 +3359,6 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "license": "MIT", - "peer": true, "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", @@ -3403,7 +3376,6 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^6.0.1" }, @@ -3419,7 +3391,6 @@ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", @@ -3437,7 +3408,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -3462,7 +3432,6 @@ "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.2.0.tgz", "integrity": "sha512-0aVxM3RH6DaiLcjj/b0KrIBZhSX1373Xci4l3cW5xiUWPctZ59zQ7jj4rqcJQ/Z8JuN/4wX3FpJSa3RssVvCug==", "license": "MIT", - "peer": true, "dependencies": { "@jest/types": "30.2.0", "chalk": "^4.1.2", @@ -3478,7 +3447,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -3494,7 +3462,6 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -3511,7 +3478,6 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", - "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -3523,8 +3489,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@jest/source-map": { "version": "30.0.1", @@ -3545,7 +3510,6 @@ "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.2.0.tgz", "integrity": "sha512-RF+Z+0CCHkARz5HT9mcQCBulb1wgCP3FBvl9VFokMX27acKphwyQsNuWH3c+ojd1LeWBLoTYoxF0zm6S/66mjg==", "license": "MIT", - "peer": true, "dependencies": { "@jest/console": "30.2.0", "@jest/types": "30.2.0", @@ -3561,7 +3525,6 @@ "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.2.0.tgz", "integrity": "sha512-wXKgU/lk8fKXMu/l5Hog1R61bL4q5GCdT6OJvdAFz1P+QrpoFuLU68eoKuVc4RbrTtNnTL5FByhWdLgOPSph+Q==", "license": "MIT", - "peer": true, "dependencies": { "@jest/test-result": "30.2.0", "graceful-fs": "^4.2.11", @@ -3577,7 +3540,6 @@ "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.2.0.tgz", "integrity": "sha512-XsauDV82o5qXbhalKxD7p4TZYYdwcaEXC77PPD2HixEFF+6YGppjrAAQurTl2ECWcEomHBMMNS9AH3kcCFx8jA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/core": "^7.27.4", "@jest/types": "30.2.0", @@ -3604,7 +3566,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -3620,7 +3581,6 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -3637,7 +3597,6 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", - "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -3649,15 +3608,13 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@jest/types": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", "license": "MIT", - "peer": true, "dependencies": { "@jest/pattern": "30.0.1", "@jest/schemas": "30.0.5", @@ -3676,7 +3633,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -3692,7 +3648,6 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -3709,7 +3664,6 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", - "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -3721,8 +3675,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", @@ -3818,6 +3771,7 @@ "integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.3", @@ -3963,6 +3917,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=8.0.0" } @@ -3984,6 +3939,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.6.1.tgz", "integrity": "sha512-XHzhwRNkBpeP8Fs/qjGrAf9r9PRv67wkJQ/7ZPaBQQ68DYlTBBx5MF9LvPx7mhuXcDessKK2b+DcxqwpgkcivQ==", "license": "Apache-2.0", + "peer": true, "engines": { "node": "^18.19.0 || >=20.6.0" }, @@ -3996,6 +3952,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.6.1.tgz", "integrity": "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, @@ -4404,6 +4361,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.6.1.tgz", "integrity": "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" @@ -4420,6 +4378,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.6.1.tgz", "integrity": "sha512-r86ut4T1e8vNwB35CqCcKd45yzqH6/6Wzvpk2/cZB8PsPLlZFTvrh8yfOS3CYZYcUmAx4hHTZJ8AO8Dj8nrdhw==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", @@ -4437,6 +4396,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.40.0.tgz", "integrity": "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=14" } @@ -5502,7 +5462,6 @@ "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", "license": "BSD-3-Clause", - "peer": true, "dependencies": { "@sinonjs/commons": "^3.0.1" } @@ -5668,6 +5627,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.3.tgz", "integrity": "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -6080,6 +6040,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6178,7 +6139,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -6270,7 +6230,6 @@ "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz", "integrity": "sha512-0YiBEOxWqKkSQWL9nNGGEgndoeL0ZpWrbLMNL5u/Kaxrli3Eaxlt3ZtIDktEvXt4L/R9r3ODr2zKwGM/2BjxVw==", "license": "MIT", - "peer": true, "dependencies": { "@jest/transform": "30.2.0", "@types/babel__core": "^7.20.5", @@ -6292,7 +6251,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -6308,7 +6266,6 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -6325,7 +6282,6 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", - "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -6337,8 +6293,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/babel-plugin-istanbul": { "version": "7.0.1", @@ -6364,7 +6319,6 @@ "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.2.0.tgz", "integrity": "sha512-ftzhzSGMUnOzcCXd6WHdBGMyuwy15Wnn0iyyWGKgBDLxf9/s5ABuraCSpBX2uG0jUg4rqJnxsLc5+oYBqoxVaA==", "license": "MIT", - "peer": true, "dependencies": { "@types/babel__core": "^7.20.5" }, @@ -6403,7 +6357,6 @@ "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.2.0.tgz", "integrity": "sha512-US4Z3NOieAQumwFnYdUWKvUKh8+YSnS/gB3t6YBiz0bskpu7Pine8pPCheNxlPEW4wnUkma2a94YuW2q3guvCQ==", "license": "MIT", - "peer": true, "dependencies": { "babel-plugin-jest-hoist": "30.2.0", "babel-preset-current-node-syntax": "^1.2.0" @@ -6635,6 +6588,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -7643,6 +7597,7 @@ "integrity": "sha512-tQMagCOC59EVgNZcC5zl7XqO30Wki9i9J3acbUvkaosCT6JX3EeFwJD7Qqp4MCikRnzS18WXV3BLIQ66ytu6+Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -7747,6 +7702,7 @@ "integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", @@ -8578,6 +8534,7 @@ "integrity": "sha512-+L0vBFYGIpSNIt/KWTpFonPrqYvgKw1eUI5Vn7mEogrQcWtWYtNQ7dNqC+px/J0idT3BAkiWrhfS7k+Tum8TUA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", @@ -8836,7 +8793,6 @@ "resolved": "https://registry.npmjs.org/expect/-/expect-30.2.0.tgz", "integrity": "sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==", "license": "MIT", - "peer": true, "dependencies": { "@jest/expect-utils": "30.2.0", "@jest/get-type": "30.1.0", @@ -10513,7 +10469,6 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-30.2.0.tgz", "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "30.2.0", "@jest/types": "30.2.0", @@ -10540,7 +10495,6 @@ "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.2.0.tgz", "integrity": "sha512-L8lR1ChrRnSdfeOvTrwZMlnWV8G/LLjQ0nG9MBclwWZidA2N5FviRki0Bvh20WRMOX31/JYvzdqTJrk5oBdydQ==", "license": "MIT", - "peer": true, "dependencies": { "execa": "^5.1.1", "jest-util": "30.2.0", @@ -10555,7 +10509,6 @@ "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.2.0.tgz", "integrity": "sha512-Fh0096NC3ZkFx05EP2OXCxJAREVxj1BcW/i6EWqqymcgYKWjyyDpral3fMxVcHXg6oZM7iULer9wGRFvfpl+Tg==", "license": "MIT", - "peer": true, "dependencies": { "@jest/environment": "30.2.0", "@jest/expect": "30.2.0", @@ -10587,7 +10540,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -10603,7 +10555,6 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -10620,7 +10571,6 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", - "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -10632,15 +10582,13 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/jest-circus/node_modules/dedent": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.1.tgz", "integrity": "sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==", "license": "MIT", - "peer": true, "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, @@ -11828,7 +11776,6 @@ "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.2.0.tgz", "integrity": "sha512-g4WkyzFQVWHtu6uqGmQR4CQxz/CH3yDSlhzXMWzNjDx843gYjReZnMRanjRCq5XZFuQrGDxgUaiYWE8BRfVckA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/core": "^7.27.4", "@jest/get-type": "30.1.0", @@ -11880,7 +11827,6 @@ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "license": "ISC", - "peer": true, "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", @@ -11898,7 +11844,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -11913,15 +11858,13 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/jest-config/node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "license": "MIT", - "peer": true, "dependencies": { "balanced-match": "^1.0.0" } @@ -11931,7 +11874,6 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -11948,7 +11890,6 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", - "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -11960,15 +11901,13 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/jest-config/node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/jest-config/node_modules/glob": { "version": "10.5.0", @@ -11976,7 +11915,6 @@ "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "license": "ISC", - "peer": true, "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", @@ -11997,7 +11935,6 @@ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "license": "BlueOak-1.0.0", - "peer": true, "dependencies": { "@isaacs/cliui": "^8.0.2" }, @@ -12012,15 +11949,13 @@ "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/jest-config/node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "license": "ISC", - "peer": true, "dependencies": { "brace-expansion": "^2.0.1" }, @@ -12036,7 +11971,6 @@ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "license": "BlueOak-1.0.0", - "peer": true, "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" @@ -12053,7 +11987,6 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "license": "MIT", - "peer": true, "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", @@ -12071,7 +12004,6 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^6.0.1" }, @@ -12087,7 +12019,6 @@ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", @@ -12105,7 +12036,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -12118,7 +12048,6 @@ "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.2.0.tgz", "integrity": "sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==", "license": "MIT", - "peer": true, "dependencies": { "@jest/diff-sequences": "30.0.1", "@jest/get-type": "30.1.0", @@ -12134,7 +12063,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -12150,7 +12078,6 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -12167,7 +12094,6 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", - "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -12179,8 +12105,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/jest-docblock": { "version": "30.2.0", @@ -12199,7 +12124,6 @@ "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.2.0.tgz", "integrity": "sha512-lpWlJlM7bCUf1mfmuqTA8+j2lNURW9eNafOy99knBM01i5CQeY5UH1vZjgT9071nDJac1M4XsbyI44oNOdhlDQ==", "license": "MIT", - "peer": true, "dependencies": { "@jest/get-type": "30.1.0", "@jest/types": "30.2.0", @@ -12216,7 +12140,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -12232,7 +12155,6 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -12249,7 +12171,6 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", - "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -12261,8 +12182,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/jest-environment-jsdom": { "version": "30.3.0", @@ -12488,7 +12408,6 @@ "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.2.0.tgz", "integrity": "sha512-ElU8v92QJ9UrYsKrxDIKCxu6PfNj4Hdcktcn0JX12zqNdqWHB0N+hwOnnBBXvjLd2vApZtuLUGs1QSY+MsXoNA==", "license": "MIT", - "peer": true, "dependencies": { "@jest/environment": "30.2.0", "@jest/fake-timers": "30.2.0", @@ -12507,7 +12426,6 @@ "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.2.0.tgz", "integrity": "sha512-sQA/jCb9kNt+neM0anSj6eZhLZUIhQgwDt7cPGjumgLM4rXsfb9kpnlacmvZz3Q5tb80nS+oG/if+NBKrHC+Xw==", "license": "MIT", - "peer": true, "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", @@ -12532,7 +12450,6 @@ "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.2.0.tgz", "integrity": "sha512-M6jKAjyzjHG0SrQgwhgZGy9hFazcudwCNovY/9HPIicmNSBuockPSedAP9vlPK6ONFJ1zfyH/M2/YYJxOz5cdQ==", "license": "MIT", - "peer": true, "dependencies": { "@jest/get-type": "30.1.0", "pretty-format": "30.2.0" @@ -12546,7 +12463,6 @@ "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.2.0.tgz", "integrity": "sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg==", "license": "MIT", - "peer": true, "dependencies": { "@jest/get-type": "30.1.0", "chalk": "^4.1.2", @@ -12562,7 +12478,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -12578,7 +12493,6 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -12595,7 +12509,6 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", - "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -12607,15 +12520,13 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/jest-message-util": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz", "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@jest/types": "30.2.0", @@ -12636,7 +12547,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -12652,7 +12562,6 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -12669,7 +12578,6 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", - "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -12681,15 +12589,13 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/jest-mock": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz", "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==", "license": "MIT", - "peer": true, "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", @@ -12730,7 +12636,6 @@ "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.2.0.tgz", "integrity": "sha512-TCrHSxPlx3tBY3hWNtRQKbtgLhsXa1WmbJEqBlTBrGafd5fiQFByy2GNCEoGR+Tns8d15GaL9cxEzKOO3GEb2A==", "license": "MIT", - "peer": true, "dependencies": { "chalk": "^4.1.2", "graceful-fs": "^4.2.11", @@ -12750,7 +12655,6 @@ "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.2.0.tgz", "integrity": "sha512-xTOIGug/0RmIe3mmCqCT95yO0vj6JURrn1TKWlNbhiAefJRWINNPgwVkrVgt/YaerPzY3iItufd80v3lOrFJ2w==", "license": "MIT", - "peer": true, "dependencies": { "jest-regex-util": "30.0.1", "jest-snapshot": "30.2.0" @@ -12764,7 +12668,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -12780,7 +12683,6 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -12797,7 +12699,6 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", - "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -12809,15 +12710,13 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/jest-runner": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.2.0.tgz", "integrity": "sha512-PqvZ2B2XEyPEbclp+gV6KO/F1FIFSbIwewRgmROCMBo/aZ6J1w8Qypoj2pEOcg3G2HzLlaP6VUtvwCI8dM3oqQ==", "license": "MIT", - "peer": true, "dependencies": { "@jest/console": "30.2.0", "@jest/environment": "30.2.0", @@ -12851,7 +12750,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -12867,7 +12765,6 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -12884,7 +12781,6 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", - "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -12896,15 +12792,13 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/jest-runtime": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.2.0.tgz", "integrity": "sha512-p1+GVX/PJqTucvsmERPMgCPvQJpFt4hFbM+VN3n8TMo47decMUcJbt+rgzwrEme0MQUA/R+1de2axftTHkKckg==", "license": "MIT", - "peer": true, "dependencies": { "@jest/environment": "30.2.0", "@jest/fake-timers": "30.2.0", @@ -12938,7 +12832,6 @@ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "license": "ISC", - "peer": true, "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", @@ -12956,7 +12849,6 @@ "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.2.0.tgz", "integrity": "sha512-b63wmnKPaK+6ZZfpYhz9K61oybvbI1aMcIs80++JI1O1rR1vaxHUCNqo3ITu6NU0d4V34yZFoHMn/uoKr/Rwfw==", "license": "MIT", - "peer": true, "dependencies": { "@jest/environment": "30.2.0", "@jest/expect": "30.2.0", @@ -12972,7 +12864,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -12987,15 +12878,13 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/jest-runtime/node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "license": "MIT", - "peer": true, "dependencies": { "balanced-match": "^1.0.0" } @@ -13005,7 +12894,6 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -13022,7 +12910,6 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", - "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -13034,15 +12921,13 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/jest-runtime/node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/jest-runtime/node_modules/glob": { "version": "10.5.0", @@ -13050,7 +12935,6 @@ "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "license": "ISC", - "peer": true, "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", @@ -13071,7 +12955,6 @@ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "license": "BlueOak-1.0.0", - "peer": true, "dependencies": { "@isaacs/cliui": "^8.0.2" }, @@ -13086,15 +12969,13 @@ "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/jest-runtime/node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "license": "ISC", - "peer": true, "dependencies": { "brace-expansion": "^2.0.1" }, @@ -13110,7 +12991,6 @@ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "license": "BlueOak-1.0.0", - "peer": true, "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" @@ -13127,7 +13007,6 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "license": "MIT", - "peer": true, "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", @@ -13145,7 +13024,6 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^6.0.1" }, @@ -13161,7 +13039,6 @@ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", @@ -13179,7 +13056,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -13192,7 +13068,6 @@ "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.2.0.tgz", "integrity": "sha512-5WEtTy2jXPFypadKNpbNkZ72puZCa6UjSr/7djeecHWOu7iYhSXSnHScT8wBz3Rn8Ena5d5RYRcsyKIeqG1IyA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/core": "^7.27.4", "@babel/generator": "^7.27.5", @@ -13225,7 +13100,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -13241,7 +13115,6 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -13258,7 +13131,6 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", - "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -13270,15 +13142,13 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/jest-util": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", "license": "MIT", - "peer": true, "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", @@ -13296,7 +13166,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -13312,7 +13181,6 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -13329,7 +13197,6 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", - "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -13341,15 +13208,13 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/jest-util/node_modules/picomatch": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -13362,7 +13227,6 @@ "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.2.0.tgz", "integrity": "sha512-FBGWi7dP2hpdi8nBoWxSsLvBFewKAg0+uSQwBaof4Y4DPgBabXgpSYC5/lR7VmnIlSpASmCi/ntRWPbv7089Pw==", "license": "MIT", - "peer": true, "dependencies": { "@jest/get-type": "30.1.0", "@jest/types": "30.2.0", @@ -13380,7 +13244,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -13396,7 +13259,6 @@ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -13409,7 +13271,6 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -13426,7 +13287,6 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", - "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -13438,15 +13298,13 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/jest-watcher": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.2.0.tgz", "integrity": "sha512-PYxa28dxJ9g777pGm/7PrbnMeA0Jr7osHP9bS7eJy9DuAjMgdGtxgf0uKMyoIsTWAkIbUW5hSDdJ3urmgXBqxg==", "license": "MIT", - "peer": true, "dependencies": { "@jest/test-result": "30.2.0", "@jest/types": "30.2.0", @@ -13466,7 +13324,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -13482,7 +13339,6 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -13499,7 +13355,6 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", - "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -13511,15 +13366,13 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/jest-worker": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.2.0.tgz", "integrity": "sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g==", "license": "MIT", - "peer": true, "dependencies": { "@types/node": "*", "@ungap/structured-clone": "^1.3.0", @@ -13536,7 +13389,6 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "license": "MIT", - "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -13552,7 +13404,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -13568,7 +13419,6 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -13585,7 +13435,6 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", - "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -13597,15 +13446,13 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/jest/node_modules/jest-cli": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.2.0.tgz", "integrity": "sha512-Os9ukIvADX/A9sLt6Zse3+nmHtHaE6hqOsjQtNiugFTbKRHYIYtZXNGNK9NChseXy7djFPjndX1tL0sCTlfpAA==", "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "30.2.0", "@jest/test-result": "30.2.0", @@ -13672,6 +13519,7 @@ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "license": "MIT", + "peer": true, "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", @@ -14291,6 +14139,7 @@ "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", "dev": true, "license": "MIT", + "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -16878,6 +16727,7 @@ "dev": true, "inBundle": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -17932,7 +17782,6 @@ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", "license": "MIT", - "peer": true, "dependencies": { "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", @@ -18463,6 +18312,7 @@ "integrity": "sha512-WRgl5GcypwramYX4HV+eQGzUbD7UUbljVmS+5G1uMwX/wLgYuJAxGeerXJDMO2xshng4+FXqCgyB5QfClV6WjA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@semantic-release/commit-analyzer": "^13.0.1", "@semantic-release/error": "^4.0.0", @@ -20081,6 +19931,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, From 2fd30331dece3f73cd3a0b2db9a57df664f30538 Mon Sep 17 00:00:00 2001 From: Pierre Brisorgueil Date: Mon, 6 Apr 2026 12:10:37 +0200 Subject: [PATCH 02/10] =?UTF-8?q?chore(organizations):=20fix=20test=20impo?= =?UTF-8?q?rt=20path=20after=20middleware/=20=E2=86=92=20middlewares/=20re?= =?UTF-8?q?name?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../organizations/tests/organizations.middleware.unit.tests.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/organizations/tests/organizations.middleware.unit.tests.js b/modules/organizations/tests/organizations.middleware.unit.tests.js index 64bf15c79..850406cc8 100644 --- a/modules/organizations/tests/organizations.middleware.unit.tests.js +++ b/modules/organizations/tests/organizations.middleware.unit.tests.js @@ -15,7 +15,7 @@ jest.unstable_mockModule('../services/organizations.membership.service.js', () = default: { findByUserAndOrganization: mockFindByUserAndOrganization }, })); -const { default: organizationsMiddleware } = await import('../middleware/organizations.middleware.js'); +const { default: organizationsMiddleware } = await import('../middlewares/organizations.middleware.js'); const { resolveOrganization } = organizationsMiddleware; /** From fdf67ab6f1ac784a1ef78feb51c19d5c814b8a34 Mon Sep 17 00:00:00 2001 From: Pierre Brisorgueil Date: Mon, 6 Apr 2026 12:15:02 +0200 Subject: [PATCH 03/10] refactor(organizations): add constants.js for membership status and role magic strings (#3400) --- modules/organizations/lib/constants.js | 12 ++++ .../middlewares/organizations.middleware.js | 4 +- .../policies/organizations.policy.js | 7 ++- .../organizations.membership.repository.js | 5 +- .../services/organizations.crud.service.js | 13 +++-- .../organizations.membership.service.js | 55 ++++++++++--------- .../services/organizations.service.js | 3 +- 7 files changed, 58 insertions(+), 41 deletions(-) create mode 100644 modules/organizations/lib/constants.js diff --git a/modules/organizations/lib/constants.js b/modules/organizations/lib/constants.js new file mode 100644 index 000000000..a65a1c6e8 --- /dev/null +++ b/modules/organizations/lib/constants.js @@ -0,0 +1,12 @@ +export const MEMBERSHIP_STATUSES = { + ACTIVE: 'active', + PENDING: 'pending', + INVITED: 'invited', + REJECTED: 'rejected', +}; + +export const MEMBERSHIP_ROLES = { + OWNER: 'owner', + ADMIN: 'admin', + MEMBER: 'member', +}; diff --git a/modules/organizations/middlewares/organizations.middleware.js b/modules/organizations/middlewares/organizations.middleware.js index 471414bca..77198a326 100644 --- a/modules/organizations/middlewares/organizations.middleware.js +++ b/modules/organizations/middlewares/organizations.middleware.js @@ -3,8 +3,8 @@ */ import OrganizationsCrudService from '../services/organizations.crud.service.js'; import MembershipService from '../services/organizations.membership.service.js'; - import responses from '../../../lib/helpers/responses.js'; +import { MEMBERSHIP_ROLES } from '../lib/constants.js'; /** * Middleware that resolves the current organization from a route param or @@ -37,7 +37,7 @@ async function resolveOrganization(req, res, next) { // Platform admin bypasses membership requirement if (req.user && req.user.roles && req.user.roles.includes('admin')) { - req.membership = { role: 'owner', organizationId: organization._id }; + req.membership = { role: MEMBERSHIP_ROLES.OWNER, organizationId: organization._id }; return next(); } diff --git a/modules/organizations/policies/organizations.policy.js b/modules/organizations/policies/organizations.policy.js index 386fe497b..08c45f188 100644 --- a/modules/organizations/policies/organizations.policy.js +++ b/modules/organizations/policies/organizations.policy.js @@ -1,6 +1,7 @@ /** * Organization ability definitions for CASL document-level authorization. */ +import { MEMBERSHIP_ROLES } from '../lib/constants.js'; /** * Register organization-related subjects for document-level and path-level resolution. @@ -50,11 +51,11 @@ export function organizationAbilities(user, membership, { can, cannot }) { if (!membership) return; switch (membership.role) { - case 'owner': + case MEMBERSHIP_ROLES.OWNER: can('manage', 'Organization', { _id: String(membership.organizationId._id || membership.organizationId) }); can('manage', 'Membership', { organizationId: String(membership.organizationId._id || membership.organizationId) }); break; - case 'admin': + case MEMBERSHIP_ROLES.ADMIN: can('read', 'Organization', { _id: String(membership.organizationId._id || membership.organizationId) }); can('update', 'Organization', { _id: String(membership.organizationId._id || membership.organizationId) }); cannot('delete', 'Organization'); @@ -62,7 +63,7 @@ export function organizationAbilities(user, membership, { can, cannot }) { can('create', 'Membership', { organizationId: String(membership.organizationId._id || membership.organizationId) }); can('delete', 'Membership', { organizationId: String(membership.organizationId._id || membership.organizationId) }); break; - case 'member': + case MEMBERSHIP_ROLES.MEMBER: can('read', 'Organization', { _id: String(membership.organizationId._id || membership.organizationId) }); can('read', 'Membership', { organizationId: String(membership.organizationId._id || membership.organizationId) }); break; diff --git a/modules/organizations/repositories/organizations.membership.repository.js b/modules/organizations/repositories/organizations.membership.repository.js index 4ef1b3f9a..96644e15b 100644 --- a/modules/organizations/repositories/organizations.membership.repository.js +++ b/modules/organizations/repositories/organizations.membership.repository.js @@ -2,6 +2,7 @@ * Module dependencies */ import mongoose from 'mongoose'; +import { MEMBERSHIP_STATUSES } from '../lib/constants.js'; const Membership = mongoose.model('Membership'); @@ -98,7 +99,7 @@ const deleteMany = (filter) => { */ const aggregateCountByOrganizations = (orgIds) => Membership.aggregate([ - { $match: { organizationId: { $in: orgIds }, status: 'active' } }, + { $match: { organizationId: { $in: orgIds }, status: MEMBERSHIP_STATUSES.ACTIVE } }, { $group: { _id: '$organizationId', count: { $sum: 1 } } }, ]); @@ -109,7 +110,7 @@ const aggregateCountByOrganizations = (orgIds) => * @returns {Promise} An array of memberships. */ const listByUsers = (userIds) => - Membership.find({ userId: { $in: userIds }, status: 'active' }).populate(defaultPopulate).sort('-createdAt').exec(); + Membership.find({ userId: { $in: userIds }, status: MEMBERSHIP_STATUSES.ACTIVE }).populate(defaultPopulate).sort('-createdAt').exec(); export default { list, diff --git a/modules/organizations/services/organizations.crud.service.js b/modules/organizations/services/organizations.crud.service.js index 057965385..377363a8e 100644 --- a/modules/organizations/services/organizations.crud.service.js +++ b/modules/organizations/services/organizations.crud.service.js @@ -24,6 +24,7 @@ import OrganizationsRepository from '../repositories/organizations.repository.js import MembershipRepository from '../repositories/organizations.membership.repository.js'; import UserService from '../../users/services/users.service.js'; import { slugify } from '../helpers/organizations.slug.js'; +import { MEMBERSHIP_STATUSES, MEMBERSHIP_ROLES } from '../lib/constants.js'; /** * @function list @@ -48,7 +49,7 @@ const list = async (search, page, perPage) => { * @returns {Promise} A promise that resolves to the list of organizations. */ const listByUser = async (user) => { - const memberships = await MembershipRepository.list({ userId: user._id || user.id, status: 'active' }); + const memberships = await MembershipRepository.list({ userId: user._id || user.id, status: MEMBERSHIP_STATUSES.ACTIVE }); const organizationIds = memberships.map((m) => m.organizationId._id || m.organizationId); const orgs = await OrganizationsRepository.list({ _id: { $in: organizationIds } }); return orgs.map((org) => { @@ -107,7 +108,7 @@ const create = async (body, user) => { membership = await MembershipRepository.create({ userId: user.id || user._id, organizationId: result._id, - role: 'owner', + role: MEMBERSHIP_ROLES.OWNER, }); await UserService.updateById(user.id || user._id, { currentOrganization: result._id }); @@ -175,7 +176,7 @@ const remove = async (organization) => { // For each affected user, switch to their next available org or set null await Promise.all(affectedUsers.map(async (u) => { - const remaining = await MembershipRepository.list({ userId: u._id, status: 'active' }); + const remaining = await MembershipRepository.list({ userId: u._id, status: MEMBERSHIP_STATUSES.ACTIVE }); const nextOrg = remaining.length > 0 ? (remaining[0].organizationId._id || remaining[0].organizationId) : null; @@ -212,7 +213,7 @@ const switchOrganization = async (user, organizationId) => { const membership = await MembershipRepository.findOne({ userId: user._id || user.id, organizationId, - status: 'active', + status: MEMBERSHIP_STATUSES.ACTIVE, }); if (!membership) { @@ -241,13 +242,13 @@ const autoSetCurrentOrganization = async (user) => { const stillActive = await MembershipRepository.findOne({ userId: user._id || user.id, organizationId: user.currentOrganization._id || user.currentOrganization, - status: 'active', + status: MEMBERSHIP_STATUSES.ACTIVE, }); if (stillActive) return user; // Membership gone — clear stale reference and fall through to find another user.currentOrganization = null; } - const memberships = await MembershipRepository.list({ userId: user._id || user.id, status: 'active' }); + const memberships = await MembershipRepository.list({ userId: user._id || user.id, status: MEMBERSHIP_STATUSES.ACTIVE }); if (memberships.length > 0) { const orgId = memberships[0].organizationId._id || memberships[0].organizationId; await UserService.updateById(user._id || user.id, { currentOrganization: orgId }); diff --git a/modules/organizations/services/organizations.membership.service.js b/modules/organizations/services/organizations.membership.service.js index 597bff35d..b2788d078 100644 --- a/modules/organizations/services/organizations.membership.service.js +++ b/modules/organizations/services/organizations.membership.service.js @@ -11,6 +11,7 @@ import { assertEmailVerified } from '../../../lib/helpers/emailVerification.js'; import MembershipRepository from '../repositories/organizations.membership.repository.js'; import OrganizationRepository from '../repositories/organizations.repository.js'; import UserService from '../../users/services/users.service.js'; +import { MEMBERSHIP_STATUSES, MEMBERSHIP_ROLES } from '../lib/constants.js'; /** * @function list @@ -22,7 +23,7 @@ import UserService from '../../users/services/users.service.js'; * @returns {Promise} A promise that resolves to the list of memberships. */ const list = async (organizationId, search, page, perPage) => { - const filter = { organizationId, status: 'active' }; + const filter = { organizationId, status: MEMBERSHIP_STATUSES.ACTIVE }; if (search) { const matchingUsers = await UserService.searchByNameOrEmail(search); filter.userId = { $in: matchingUsers.map((u) => u._id) }; @@ -36,7 +37,7 @@ const list = async (organizationId, search, page, perPage) => { * @param {String} userId - The ID of the user. * @returns {Promise} A promise that resolves to the list of memberships. */ -const listByUser = (userId) => MembershipRepository.list({ userId, status: 'active' }); +const listByUser = (userId) => MembershipRepository.list({ userId, status: MEMBERSHIP_STATUSES.ACTIVE }); /** * @function get @@ -54,7 +55,7 @@ const get = (id) => MembershipRepository.get(id); * @returns {Promise} A promise resolving to the membership or null. */ const findByUserAndOrganization = (userId, organizationId) => - MembershipRepository.findOne({ userId, organizationId, status: 'active' }); + MembershipRepository.findOne({ userId, organizationId, status: MEMBERSHIP_STATUSES.ACTIVE }); /** * @function create @@ -72,9 +73,9 @@ const create = (data) => MembershipRepository.create(data); * @returns {Promise} A promise resolving to the updated membership. */ const updateRole = async (membership, role) => { - if (membership.role === 'owner' && role !== 'owner') { + if (membership.role === MEMBERSHIP_ROLES.OWNER && role !== MEMBERSHIP_ROLES.OWNER) { const orgId = membership.organizationId._id || membership.organizationId; - const ownerCount = await MembershipRepository.count({ organizationId: orgId, role: 'owner', status: 'active' }); + const ownerCount = await MembershipRepository.count({ organizationId: orgId, role: MEMBERSHIP_ROLES.OWNER, status: MEMBERSHIP_STATUSES.ACTIVE }); if (ownerCount <= 1) throw new Error('Cannot change role of the last owner'); } membership.role = role; @@ -88,9 +89,9 @@ const updateRole = async (membership, role) => { * @returns {Promise} A promise resolving to a confirmation of the deletion. */ const remove = async (membership) => { - if (membership.role === 'owner') { + if (membership.role === MEMBERSHIP_ROLES.OWNER) { const orgId = membership.organizationId._id || membership.organizationId; - const ownerCount = await MembershipRepository.count({ organizationId: orgId, role: 'owner', status: 'active' }); + const ownerCount = await MembershipRepository.count({ organizationId: orgId, role: MEMBERSHIP_ROLES.OWNER, status: MEMBERSHIP_STATUSES.ACTIVE }); if (ownerCount <= 1) throw new Error('Cannot remove the last owner of an organization'); } const userId = membership.userId._id || membership.userId; @@ -100,7 +101,7 @@ const remove = async (membership) => { // Clear currentOrganization if it pointed to the org the user was removed from const userDoc = await UserService.getBrut({ id: String(userId) }); if (userDoc && String(userDoc.currentOrganization) === String(removedOrgId)) { - const remaining = await MembershipRepository.list({ userId, status: 'active' }); + const remaining = await MembershipRepository.list({ userId, status: MEMBERSHIP_STATUSES.ACTIVE }); const nextOrg = remaining.length > 0 ? (remaining[0].organizationId._id || remaining[0].organizationId) : null; await UserService.updateById(userDoc._id, { currentOrganization: nextOrg }); } @@ -113,7 +114,7 @@ const remove = async (membership) => { * @param {String} organizationId - The ID of the organization. * @returns {Promise} A promise that resolves to the list of pending memberships. */ -const listPending = (organizationId) => MembershipRepository.list({ organizationId, status: 'pending' }); +const listPending = (organizationId) => MembershipRepository.list({ organizationId, status: MEMBERSHIP_STATUSES.PENDING }); /** * @function listPendingByUser @@ -121,7 +122,7 @@ const listPending = (organizationId) => MembershipRepository.list({ organization * @param {String} userId - The ID of the user. * @returns {Promise} A promise that resolves to the list of pending memberships. */ -const listPendingByUser = (userId) => MembershipRepository.list({ userId, status: 'pending' }); +const listPendingByUser = (userId) => MembershipRepository.list({ userId, status: MEMBERSHIP_STATUSES.PENDING }); /** * @function createJoinRequest @@ -138,20 +139,20 @@ const createJoinRequest = async (userId, organizationId) => { if (!user) throw new Error('User not found'); assertEmailVerified(user); - const existing = await MembershipRepository.findOne({ userId, organizationId, status: { $in: ['active', 'pending'] } }); + const existing = await MembershipRepository.findOne({ userId, organizationId, status: { $in: [MEMBERSHIP_STATUSES.ACTIVE, MEMBERSHIP_STATUSES.PENDING] } }); if (existing) { - if (existing.status === 'active') throw new Error('Already a member of this organization'); + if (existing.status === MEMBERSHIP_STATUSES.ACTIVE) throw new Error('Already a member of this organization'); throw new Error('A pending request already exists'); } // Limit to 1 pending request at a time across all organizations - const pendingAnywhere = await MembershipRepository.findOne({ userId, status: 'pending' }); + const pendingAnywhere = await MembershipRepository.findOne({ userId, status: MEMBERSHIP_STATUSES.PENDING }); if (pendingAnywhere) throw new Error('You already have a pending request. Please wait for it to be reviewed before requesting to join another organization.'); - const membership = await MembershipRepository.create({ userId, organizationId, role: 'member', status: 'pending' }); + const membership = await MembershipRepository.create({ userId, organizationId, role: MEMBERSHIP_ROLES.MEMBER, status: MEMBERSHIP_STATUSES.PENDING }); if (mailer.isConfigured()) { const org = await OrganizationRepository.get(organizationId); if (user?.email && org?.name) { - const admins = await MembershipRepository.list({ organizationId, role: { $in: ['owner', 'admin'] }, status: 'active' }); + const admins = await MembershipRepository.list({ organizationId, role: { $in: [MEMBERSHIP_ROLES.OWNER, MEMBERSHIP_ROLES.ADMIN] }, status: MEMBERSHIP_STATUSES.ACTIVE }); for (const admin of admins) { if (admin.userId?.email) { mailer.sendMail({ @@ -181,7 +182,7 @@ const createJoinRequest = async (userId, organizationId) => { * @returns {Promise} The updated membership. */ const approveRequest = async (membership) => { - membership.status = 'active'; + membership.status = MEMBERSHIP_STATUSES.ACTIVE; const result = await MembershipRepository.update(membership); // Set currentOrganization if user doesn't have one @@ -250,17 +251,17 @@ const rejectRequest = async (membership) => { * @returns {Promise} A success confirmation. */ const leave = async (userId, organizationId) => { - const membership = await MembershipRepository.findOne({ userId, organizationId, status: 'active' }); + const membership = await MembershipRepository.findOne({ userId, organizationId, status: MEMBERSHIP_STATUSES.ACTIVE }); if (!membership) throw new Error('You are not a member of this organization'); - if (membership.role === 'owner') { - const ownerCount = await MembershipRepository.count({ organizationId, role: 'owner', status: 'active' }); + if (membership.role === MEMBERSHIP_ROLES.OWNER) { + const ownerCount = await MembershipRepository.count({ organizationId, role: MEMBERSHIP_ROLES.OWNER, status: MEMBERSHIP_STATUSES.ACTIVE }); if (ownerCount <= 1) throw new Error('You are the last owner. Promote another member before leaving.'); } await MembershipRepository.remove(membership); const userDoc = await UserService.getBrut({ id: String(userId) }); if (userDoc && String(userDoc.currentOrganization) === String(organizationId)) { - const remaining = await MembershipRepository.list({ userId, status: 'active' }); + const remaining = await MembershipRepository.list({ userId, status: MEMBERSHIP_STATUSES.ACTIVE }); const nextOrg = remaining.length > 0 ? (remaining[0].organizationId._id || remaining[0].organizationId) : null; await UserService.updateById(userDoc._id, { currentOrganization: nextOrg }); } @@ -280,7 +281,7 @@ const invite = async (organizationId, email, invitedBy) => { const existingInvite = await MembershipRepository.findOne({ invitedEmail: email.toLowerCase(), organizationId, - status: 'invited', + status: MEMBERSHIP_STATUSES.INVITED, }); if (existingInvite) throw new Error('An invite has already been sent to this email'); @@ -289,7 +290,7 @@ const invite = async (organizationId, email, invitedBy) => { const existingMembership = await MembershipRepository.findOne({ userId: existingUser._id, organizationId, - status: { $in: ['active', 'pending', 'invited'] }, + status: { $in: [MEMBERSHIP_STATUSES.ACTIVE, MEMBERSHIP_STATUSES.PENDING, MEMBERSHIP_STATUSES.INVITED] }, }); if (existingMembership) throw new Error('User is already a member or has a pending request'); } @@ -298,8 +299,8 @@ const invite = async (organizationId, email, invitedBy) => { const membership = await MembershipRepository.create({ userId: existingUser ? existingUser._id : null, organizationId, - role: 'member', - status: 'invited', + role: MEMBERSHIP_ROLES.MEMBER, + status: MEMBERSHIP_STATUSES.INVITED, inviteToken, invitedEmail: email.toLowerCase(), inviteExpiresAt: new Date(Date.now() + 7 * 24 * 3600000), @@ -340,7 +341,7 @@ const invite = async (organizationId, email, invitedBy) => { * @returns {Promise} The updated membership. */ const acceptInvite = async (token, userId) => { - const membership = await MembershipRepository.findOne({ inviteToken: token, status: 'invited' }); + const membership = await MembershipRepository.findOne({ inviteToken: token, status: MEMBERSHIP_STATUSES.INVITED }); if (!membership) throw new Error('Invalid or expired invite'); if (membership.inviteExpiresAt && membership.inviteExpiresAt < Date.now()) { @@ -360,7 +361,7 @@ const acceptInvite = async (token, userId) => { } membership.userId = userId; - membership.status = 'active'; + membership.status = MEMBERSHIP_STATUSES.ACTIVE; membership.inviteToken = null; const result = await MembershipRepository.update(membership); @@ -379,7 +380,7 @@ const acceptInvite = async (token, userId) => { * @param {String} token - The invite token. * @returns {Promise} The invited membership or null. */ -const getInvite = (token) => MembershipRepository.findOne({ inviteToken: token, status: 'invited' }); +const getInvite = (token) => MembershipRepository.findOne({ inviteToken: token, status: MEMBERSHIP_STATUSES.INVITED }); /** * @function count diff --git a/modules/organizations/services/organizations.service.js b/modules/organizations/services/organizations.service.js index 6afc71f52..090f3617c 100644 --- a/modules/organizations/services/organizations.service.js +++ b/modules/organizations/services/organizations.service.js @@ -11,6 +11,7 @@ import MembershipRepository from '../repositories/organizations.membership.repos import MembershipService from './organizations.membership.service.js'; import UserService from '../../users/services/users.service.js'; import { slugify, generateOrganizationSlug } from '../helpers/organizations.slug.js'; +import { MEMBERSHIP_ROLES } from '../lib/constants.js'; /** * @desc Strip sensitive fields from an organization document before returning to public flows. @@ -90,7 +91,7 @@ const createOrganizationForUser = async ({ name, slug, domain, user, slugGenerat membership = await MembershipRepository.create({ userId, organizationId: organization._id, - role: 'owner', + role: MEMBERSHIP_ROLES.OWNER, }); await UserService.updateById(userId, { currentOrganization: organization._id }); From 9bf46602208349f1491a50cdda9dc6cf2b217552 Mon Sep 17 00:00:00 2001 From: Pierre Brisorgueil Date: Mon, 6 Apr 2026 12:15:49 +0200 Subject: [PATCH 04/10] refactor(membership): extract validateLastOwnerProtection private helper (#3399) --- .../organizations.membership.service.js | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/modules/organizations/services/organizations.membership.service.js b/modules/organizations/services/organizations.membership.service.js index b2788d078..0d6266c70 100644 --- a/modules/organizations/services/organizations.membership.service.js +++ b/modules/organizations/services/organizations.membership.service.js @@ -13,6 +13,24 @@ import OrganizationRepository from '../repositories/organizations.repository.js' import UserService from '../../users/services/users.service.js'; import { MEMBERSHIP_STATUSES, MEMBERSHIP_ROLES } from '../lib/constants.js'; +/** + * @function validateLastOwnerProtection + * @description Throws if there is only one active owner left in the organization. + * @param {String} organizationId - The ID of the organization to check. + * @returns {Promise} + * @throws {Error} If the organization has only one active owner. + */ +const validateLastOwnerProtection = async (organizationId) => { + const ownerCount = await MembershipRepository.count({ + organizationId, + role: MEMBERSHIP_ROLES.OWNER, + status: MEMBERSHIP_STATUSES.ACTIVE, + }); + if (ownerCount <= 1) { + throw new Error('Cannot remove the last owner of an organization'); + } +}; + /** * @function list * @description Service to retrieve active memberships for an organization. @@ -75,8 +93,7 @@ const create = (data) => MembershipRepository.create(data); const updateRole = async (membership, role) => { if (membership.role === MEMBERSHIP_ROLES.OWNER && role !== MEMBERSHIP_ROLES.OWNER) { const orgId = membership.organizationId._id || membership.organizationId; - const ownerCount = await MembershipRepository.count({ organizationId: orgId, role: MEMBERSHIP_ROLES.OWNER, status: MEMBERSHIP_STATUSES.ACTIVE }); - if (ownerCount <= 1) throw new Error('Cannot change role of the last owner'); + await validateLastOwnerProtection(orgId); } membership.role = role; return MembershipRepository.update(membership); @@ -91,8 +108,7 @@ const updateRole = async (membership, role) => { const remove = async (membership) => { if (membership.role === MEMBERSHIP_ROLES.OWNER) { const orgId = membership.organizationId._id || membership.organizationId; - const ownerCount = await MembershipRepository.count({ organizationId: orgId, role: MEMBERSHIP_ROLES.OWNER, status: MEMBERSHIP_STATUSES.ACTIVE }); - if (ownerCount <= 1) throw new Error('Cannot remove the last owner of an organization'); + await validateLastOwnerProtection(orgId); } const userId = membership.userId._id || membership.userId; const removedOrgId = membership.organizationId._id || membership.organizationId; @@ -254,8 +270,7 @@ const leave = async (userId, organizationId) => { const membership = await MembershipRepository.findOne({ userId, organizationId, status: MEMBERSHIP_STATUSES.ACTIVE }); if (!membership) throw new Error('You are not a member of this organization'); if (membership.role === MEMBERSHIP_ROLES.OWNER) { - const ownerCount = await MembershipRepository.count({ organizationId, role: MEMBERSHIP_ROLES.OWNER, status: MEMBERSHIP_STATUSES.ACTIVE }); - if (ownerCount <= 1) throw new Error('You are the last owner. Promote another member before leaving.'); + await validateLastOwnerProtection(organizationId); } await MembershipRepository.remove(membership); From 40c12e536ac2c7478dec4fe8e9986c8eeccbfc45 Mon Sep 17 00:00:00 2001 From: Pierre Brisorgueil Date: Mon, 6 Apr 2026 12:17:20 +0200 Subject: [PATCH 05/10] refactor(users): move removeSensitive() from AuthService to UsersService (#3401) --- modules/home/services/home.service.js | 4 ++-- modules/users/services/users.service.js | 32 ++++++++++++++++++------- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/modules/home/services/home.service.js b/modules/home/services/home.service.js index 61f638079..d9ee4b69e 100644 --- a/modules/home/services/home.service.js +++ b/modules/home/services/home.service.js @@ -8,7 +8,7 @@ import { Base64 } from 'js-base64'; import { promises as fs } from 'fs'; import mongoose from 'mongoose'; -import AuthService from '../../auth/services/auth.service.js'; +import UserService from '../../users/services/users.service.js'; import config from '../../../config/index.js'; import mailer from '../../../lib/helpers/mailer/index.js'; import HomeRepository from '../repositories/home.repository.js'; @@ -91,7 +91,7 @@ const changelogs = async () => { */ const team = async () => { const result = await HomeRepository.team(); - return Promise.resolve(result.map((user) => AuthService.removeSensitive(user))); + return Promise.resolve(result.map((user) => UserService.removeSensitive(user))); }; /** diff --git a/modules/users/services/users.service.js b/modules/users/services/users.service.js index 677cb92f9..e8bd6149a 100644 --- a/modules/users/services/users.service.js +++ b/modules/users/services/users.service.js @@ -9,6 +9,19 @@ import UserRepository from '../repositories/users.repository.js'; import MembershipService from '../../organizations/services/organizations.membership.service.js'; import OrganizationsCrudService from '../../organizations/services/organizations.crud.service.js'; +/** + * @desc Remove sensitive data from user object, returning only whitelisted keys. + * @param {Object} user - Mongoose document or plain user object + * @param {Array} [conf] - Optional list of keys to pick. Defaults to config.whitelists.users.default. + * @return {Object|null} sanitized user object or null + */ +const removeSensitive = (user, conf) => { + if (!user || typeof user !== 'object') return null; + const keys = conf || config.whitelists.users.default; + const plain = typeof user.toJSON === 'function' ? user.toJSON() : user; + return _.pick(plain, keys); +}; + /** * @desc Function to get all users in db * @param {String} search @@ -18,7 +31,7 @@ import OrganizationsCrudService from '../../organizations/services/organizations */ const list = async (search, page, perPage) => { const result = await UserRepository.list(search, page || 0, perPage || 20); - return result.map((user) => AuthService.removeSensitive(user)); + return result.map((user) => removeSensitive(user)); }; /** @@ -41,7 +54,7 @@ const create = async (user) => { } const result = await UserRepository.create(user); // Remove sensitive data before return - return AuthService.removeSensitive(result); + return removeSensitive(result); }; /** @@ -51,7 +64,7 @@ const create = async (user) => { */ const search = async (input) => { const result = await UserRepository.search(input); - return result.map((user) => AuthService.removeSensitive(user)); + return result.map((user) => removeSensitive(user)); }; /** @@ -61,7 +74,7 @@ const search = async (input) => { */ const get = async (user) => { const result = await UserRepository.get(user); - return AuthService.removeSensitive(result); + return removeSensitive(result); }; /** @@ -82,12 +95,12 @@ const getBrut = async (user) => { * @return {Promise} user - */ const update = async (user, body, option) => { - if (!option) user = _.assignIn(user, AuthService.removeSensitive(body, config.whitelists.users.update)); - else if (option === 'admin') user = _.assignIn(user, AuthService.removeSensitive(body, config.whitelists.users.updateAdmin)); - else if (option === 'recover') user = _.assignIn(user, AuthService.removeSensitive(body, config.whitelists.users.recover)); + if (!option) user = _.assignIn(user, removeSensitive(body, config.whitelists.users.update)); + else if (option === 'admin') user = _.assignIn(user, removeSensitive(body, config.whitelists.users.updateAdmin)); + else if (option === 'recover') user = _.assignIn(user, removeSensitive(body, config.whitelists.users.recover)); const result = await UserRepository.update(user); - return AuthService.removeSensitive(result); + return removeSensitive(result); }; /** @@ -98,7 +111,7 @@ const update = async (user, body, option) => { const terms = async (user) => { user = _.assignIn(user, { terms: new Date() }); const result = await UserRepository.update(user); - return AuthService.removeSensitive(result); + return removeSensitive(result); }; /** @@ -196,4 +209,5 @@ export default { findByIdAndUpdatePopulated, searchByNameOrEmail, findByEmail, + removeSensitive, }; From 969487bf0606b7277709f4c6780a795165cd0387 Mon Sep 17 00:00:00 2001 From: Pierre Brisorgueil Date: Mon, 6 Apr 2026 12:19:38 +0200 Subject: [PATCH 06/10] fix(audit): remove req dependency from AuditService.log() (#3398) --- modules/audit/middlewares/audit.middleware.js | 6 ++- modules/audit/services/audit.service.js | 20 ++++---- .../tests/audit.middleware.unit.tests.js | 4 +- .../audit/tests/audit.service.unit.tests.js | 47 +++++++------------ 4 files changed, 34 insertions(+), 43 deletions(-) diff --git a/modules/audit/middlewares/audit.middleware.js b/modules/audit/middlewares/audit.middleware.js index 59181ccc2..948a9e9bd 100644 --- a/modules/audit/middlewares/audit.middleware.js +++ b/modules/audit/middlewares/audit.middleware.js @@ -3,6 +3,7 @@ */ import AuditService from '../services/audit.service.js'; import logger from '../../../lib/services/logger.js'; +import config from '../../../config/index.js'; /** * Default route prefixes to skip when auto-capturing audit events. @@ -124,7 +125,10 @@ const createAuditMiddleware = (options = {}) => { AuditService.log({ action, - req, + userId: req.user?._id || req.user?.id, + organizationId: req.organization?._id || req.organization?.id, + ip: config.audit?.captureIp !== false ? (req.ip || req.connection?.remoteAddress || '') : undefined, + userAgent: config.audit?.captureUserAgent !== false ? (req.headers?.['user-agent'] || '') : undefined, targetType, targetId, }).catch((err) => logger.error('audit.middleware: audit log write failed', { message: err?.message, stack: err?.stack })); diff --git a/modules/audit/services/audit.service.js b/modules/audit/services/audit.service.js index d2153aa32..208570e0d 100644 --- a/modules/audit/services/audit.service.js +++ b/modules/audit/services/audit.service.js @@ -10,13 +10,16 @@ import AuditRepository from '../repositories/audit.repository.js'; * @description Record an audit log entry. No-op when audit is disabled. * @param {Object} params * @param {string} params.action - Action identifier (e.g. 'auth.login', 'billing.subscribe') - * @param {Object} [params.req] - Express request object (extracts userId, orgId, ip, userAgent) + * @param {string} [params.userId] - ID of the acting user + * @param {string} [params.organizationId] - ID of the organization context + * @param {string} [params.ip] - IP address of the request + * @param {string} [params.userAgent] - User-agent of the request * @param {string} [params.targetType] - Type of the target entity * @param {string} [params.targetId] - ID of the target entity * @param {Object} [params.metadata] - Additional metadata * @returns {Promise} The created audit log entry or null if disabled */ -const log = async ({ action, req, targetType, targetId, metadata } = {}) => { +const log = async ({ action, userId, organizationId, ip, userAgent, targetType, targetId, metadata } = {}) => { if (!config.audit?.enabled) return null; if (!action) return null; @@ -27,15 +30,10 @@ const log = async ({ action, req, targetType, targetId, metadata } = {}) => { metadata: metadata || {}, }; - // Extract context from request if available (coerce ObjectIds to strings) - if (req) { - const uid = req.user?._id || req.user?.id; - const oid = req.organization?._id || req.organization?.id; - if (uid) entry.userId = String(uid); - if (oid) entry.orgId = String(oid); - entry.ip = config.audit?.captureIp !== false ? (req.ip || req.connection?.remoteAddress || '') : ''; - entry.userAgent = config.audit?.captureUserAgent !== false ? (req.headers?.['user-agent'] || '') : ''; - } + if (userId) entry.userId = String(userId); + if (organizationId) entry.orgId = String(organizationId); + if (ip !== undefined) entry.ip = ip || ''; + if (userAgent !== undefined) entry.userAgent = userAgent || ''; try { return await AuditRepository.create(entry); diff --git a/modules/audit/tests/audit.middleware.unit.tests.js b/modules/audit/tests/audit.middleware.unit.tests.js index e6e22b825..033cb99a7 100644 --- a/modules/audit/tests/audit.middleware.unit.tests.js +++ b/modules/audit/tests/audit.middleware.unit.tests.js @@ -148,7 +148,9 @@ describe('Audit middleware unit tests:', () => { const call = mockLog.mock.calls[0][0]; expect(call.action).toBe('auth.signin'); expect(call.targetType).toBe('User'); - expect(call.req).toBe(req); + expect(call.req).toBeUndefined(); + expect(call.userId).toBeDefined(); + expect(call.ip).toBeDefined(); }); test('should log PUT mutations', () => { diff --git a/modules/audit/tests/audit.service.unit.tests.js b/modules/audit/tests/audit.service.unit.tests.js index 63019ba04..ef8c76458 100644 --- a/modules/audit/tests/audit.service.unit.tests.js +++ b/modules/audit/tests/audit.service.unit.tests.js @@ -54,16 +54,13 @@ describe('AuditService unit tests:', () => { test('should log an audit entry with request context', async () => { const userId = '507f1f77bcf86cd799439011'; const orgId = '507f1f77bcf86cd799439012'; - const req = { - user: { _id: userId }, - organization: { _id: orgId }, - ip: '127.0.0.1', - headers: { 'user-agent': 'TestAgent/1.0' }, - }; await AuditService.log({ action: 'auth.login', - req, + userId, + organizationId: orgId, + ip: '127.0.0.1', + userAgent: 'TestAgent/1.0', targetType: 'User', targetId: userId, metadata: { foo: 'bar' }, @@ -115,47 +112,37 @@ describe('AuditService unit tests:', () => { expect(mockList).toHaveBeenCalledWith({ userId }, 1, 10); }); - // GDPR config flag tests - test('should capture IP when captureIp is true (default)', async () => { - mockConfig.audit = { enabled: true, ttlDays: 90, captureIp: true, captureUserAgent: true }; - const req = { ip: '10.0.0.1', headers: { 'user-agent': 'Bot/1.0' } }; - await AuditService.log({ action: 'test.ip', req }); + // GDPR config flag tests — ip/userAgent are now extracted by the caller + test('should store ip when provided', async () => { + await AuditService.log({ action: 'test.ip', ip: '10.0.0.1', userAgent: 'Bot/1.0' }); expect(mockCreate).toHaveBeenCalledTimes(1); const arg = mockCreate.mock.calls[0][0]; expect(arg.ip).toBe('10.0.0.1'); }); - test('should set IP to empty string when captureIp is false', async () => { - mockConfig.audit = { enabled: true, ttlDays: 90, captureIp: false, captureUserAgent: true }; - const req = { ip: '10.0.0.1', headers: { 'user-agent': 'Bot/1.0' } }; - await AuditService.log({ action: 'test.ip', req }); + test('should store empty string ip when undefined is passed', async () => { + await AuditService.log({ action: 'test.ip', ip: undefined, userAgent: 'Bot/1.0' }); expect(mockCreate).toHaveBeenCalledTimes(1); const arg = mockCreate.mock.calls[0][0]; - expect(arg.ip).toBe(''); + expect(arg.ip).toBeFalsy(); }); - test('should capture User-Agent when captureUserAgent is true (default)', async () => { - mockConfig.audit = { enabled: true, ttlDays: 90, captureIp: true, captureUserAgent: true }; - const req = { ip: '10.0.0.1', headers: { 'user-agent': 'Bot/1.0' } }; - await AuditService.log({ action: 'test.ua', req }); + test('should store userAgent when provided', async () => { + await AuditService.log({ action: 'test.ua', ip: '10.0.0.1', userAgent: 'Bot/1.0' }); expect(mockCreate).toHaveBeenCalledTimes(1); const arg = mockCreate.mock.calls[0][0]; expect(arg.userAgent).toBe('Bot/1.0'); }); - test('should set User-Agent to empty string when captureUserAgent is false', async () => { - mockConfig.audit = { enabled: true, ttlDays: 90, captureIp: true, captureUserAgent: false }; - const req = { ip: '10.0.0.1', headers: { 'user-agent': 'Bot/1.0' } }; - await AuditService.log({ action: 'test.ua', req }); + test('should set User-Agent to empty string when undefined is passed', async () => { + await AuditService.log({ action: 'test.ua', ip: '10.0.0.1', userAgent: undefined }); expect(mockCreate).toHaveBeenCalledTimes(1); const arg = mockCreate.mock.calls[0][0]; - expect(arg.userAgent).toBe(''); + expect(arg.userAgent).toBeFalsy(); }); - test('should default to capturing IP and User-Agent when config keys are undefined', async () => { - mockConfig.audit = { enabled: true, ttlDays: 90 }; - const req = { ip: '192.168.1.1', headers: { 'user-agent': 'DefaultBot/2.0' } }; - await AuditService.log({ action: 'test.defaults', req }); + test('should store ip and userAgent when provided', async () => { + await AuditService.log({ action: 'test.defaults', ip: '192.168.1.1', userAgent: 'DefaultBot/2.0' }); expect(mockCreate).toHaveBeenCalledTimes(1); const arg = mockCreate.mock.calls[0][0]; expect(arg.ip).toBe('192.168.1.1'); From 75e4880947b1dcfe4b7ae9a07781ddc2ea89a33c Mon Sep 17 00:00:00 2001 From: Pierre Brisorgueil Date: Mon, 6 Apr 2026 12:35:28 +0200 Subject: [PATCH 07/10] =?UTF-8?q?fix(modules):=20address=20reviewer=20feed?= =?UTF-8?q?back=20=E2=80=94=20constants,=20removeSensitive=20util,=20test?= =?UTF-8?q?=20precision,=20JSDoc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../audit/tests/audit.integration.tests.js | 6 +- .../tests/audit.middleware.unit.tests.js | 6 +- .../audit/tests/audit.service.unit.tests.js | 8 +-- modules/home/services/home.service.js | 15 ++--- .../organizations.membership.controller.js | 5 +- ...anizations.membershipRequest.controller.js | 5 +- .../organizations.membership.service.js | 2 +- modules/users/services/users.service.js | 61 ++++++++----------- modules/users/utils/sanitizeUser.js | 21 +++++++ 9 files changed, 73 insertions(+), 56 deletions(-) create mode 100644 modules/users/utils/sanitizeUser.js diff --git a/modules/audit/tests/audit.integration.tests.js b/modules/audit/tests/audit.integration.tests.js index f4e8f88b8..e781ce6fc 100644 --- a/modules/audit/tests/audit.integration.tests.js +++ b/modules/audit/tests/audit.integration.tests.js @@ -68,13 +68,15 @@ describe('Audit integration tests:', () => { // Create some audit entries await AuditService.log({ action: 'auth.login', - req: { user: { _id: adminUser.id }, headers: { 'user-agent': 'test' } }, + userId: adminUser.id, + userAgent: 'test', targetType: 'User', targetId: adminUser.id, }); await AuditService.log({ action: 'auth.signup', - req: { user: { _id: adminUser.id }, headers: { 'user-agent': 'test' } }, + userId: adminUser.id, + userAgent: 'test', targetType: 'User', targetId: adminUser.id, }); diff --git a/modules/audit/tests/audit.middleware.unit.tests.js b/modules/audit/tests/audit.middleware.unit.tests.js index 033cb99a7..0a17bf8c2 100644 --- a/modules/audit/tests/audit.middleware.unit.tests.js +++ b/modules/audit/tests/audit.middleware.unit.tests.js @@ -149,8 +149,10 @@ describe('Audit middleware unit tests:', () => { expect(call.action).toBe('auth.signin'); expect(call.targetType).toBe('User'); expect(call.req).toBeUndefined(); - expect(call.userId).toBeDefined(); - expect(call.ip).toBeDefined(); + expect(call.userId).toBe('507f1f77bcf86cd799439011'); + expect(call.organizationId).toBe('507f1f77bcf86cd799439012'); + expect(call.ip).toBe('127.0.0.1'); + expect(call.userAgent).toBe('TestAgent/1.0'); }); test('should log PUT mutations', () => { diff --git a/modules/audit/tests/audit.service.unit.tests.js b/modules/audit/tests/audit.service.unit.tests.js index ef8c76458..56e2c915e 100644 --- a/modules/audit/tests/audit.service.unit.tests.js +++ b/modules/audit/tests/audit.service.unit.tests.js @@ -120,11 +120,11 @@ describe('AuditService unit tests:', () => { expect(arg.ip).toBe('10.0.0.1'); }); - test('should store empty string ip when undefined is passed', async () => { + test('should omit ip when undefined is passed', async () => { await AuditService.log({ action: 'test.ip', ip: undefined, userAgent: 'Bot/1.0' }); expect(mockCreate).toHaveBeenCalledTimes(1); const arg = mockCreate.mock.calls[0][0]; - expect(arg.ip).toBeFalsy(); + expect(arg.ip).toBeUndefined(); }); test('should store userAgent when provided', async () => { @@ -134,11 +134,11 @@ describe('AuditService unit tests:', () => { expect(arg.userAgent).toBe('Bot/1.0'); }); - test('should set User-Agent to empty string when undefined is passed', async () => { + test('should omit userAgent when undefined is passed', async () => { await AuditService.log({ action: 'test.ua', ip: '10.0.0.1', userAgent: undefined }); expect(mockCreate).toHaveBeenCalledTimes(1); const arg = mockCreate.mock.calls[0][0]; - expect(arg.userAgent).toBeFalsy(); + expect(arg.userAgent).toBeUndefined(); }); test('should store ip and userAgent when provided', async () => { diff --git a/modules/home/services/home.service.js b/modules/home/services/home.service.js index d9ee4b69e..6271405d5 100644 --- a/modules/home/services/home.service.js +++ b/modules/home/services/home.service.js @@ -8,10 +8,10 @@ import { Base64 } from 'js-base64'; import { promises as fs } from 'fs'; import mongoose from 'mongoose'; -import UserService from '../../users/services/users.service.js'; import config from '../../../config/index.js'; import mailer from '../../../lib/helpers/mailer/index.js'; import HomeRepository from '../repositories/home.repository.js'; +import { removeSensitive } from '../../users/utils/sanitizeUser.js'; /** * @desc Check whether a config value is meaningfully set (non-empty, not a DEVKIT placeholder). @@ -21,19 +21,20 @@ import HomeRepository from '../repositories/home.repository.js'; const isSet = (value) => !!(value && typeof value === 'string' && value.trim() !== '' && !value.startsWith('DEVKIT_NODE_')); /** - * @desc Function to get all admin users in db - * @return {Promise} All users + * @desc Function to get page content from markdown file + * @param {string} name - The name of the markdown file + * @returns {Promise} Page content array */ const page = async (name) => { const markdown = await fs.readFile(path.resolve(`./config/markdown/${name}.md`), 'utf8'); const test = await fs.stat(path.resolve(`./config/markdown/${name}.md`)); - return Promise.resolve([ + return [ { title: _.startCase(name), updatedAt: test.mtime, markdown, }, - ]); + ]; }; /** @@ -87,11 +88,11 @@ const changelogs = async () => { /** * @desc Function to get all admin users in db - * @return {Promise} All users + * @returns {Promise} All users (sanitized) */ const team = async () => { const result = await HomeRepository.team(); - return Promise.resolve(result.map((user) => UserService.removeSensitive(user))); + return result.map((user) => removeSensitive(user)); }; /** diff --git a/modules/organizations/controllers/organizations.membership.controller.js b/modules/organizations/controllers/organizations.membership.controller.js index d4d6fbdae..367dee292 100644 --- a/modules/organizations/controllers/organizations.membership.controller.js +++ b/modules/organizations/controllers/organizations.membership.controller.js @@ -4,6 +4,7 @@ import errors from '../../../lib/helpers/errors.js'; import responses from '../../../lib/helpers/responses.js'; import MembershipService from '../services/organizations.membership.service.js'; +import { MEMBERSHIP_ROLES } from '../lib/constants.js'; /** * @function list @@ -34,7 +35,7 @@ const list = async (req, res) => { const updateRole = async (req, res) => { try { // Belt-and-suspenders: only owners can change roles (CASL blocks admins via no 'update Membership') - if (req.membership && req.membership.role !== 'owner') { + if (req.membership && req.membership.role !== MEMBERSHIP_ROLES.OWNER) { return responses.error(res, 403, 'Forbidden', 'Only owners can change member roles')(); } const membership = await MembershipService.updateRole(req.membershipDoc, req.body.role); @@ -54,7 +55,7 @@ const updateRole = async (req, res) => { const remove = async (req, res) => { try { // Admins can only remove members, not other admins or owners - if (req.membership && req.membership.role !== 'owner' && req.membershipDoc.role !== 'member') { + if (req.membership && req.membership.role !== MEMBERSHIP_ROLES.OWNER && req.membershipDoc.role !== MEMBERSHIP_ROLES.MEMBER) { return responses.error(res, 403, 'Forbidden', 'Only owners can remove admins or other owners')(); } const result = await MembershipService.remove(req.membershipDoc); diff --git a/modules/organizations/controllers/organizations.membershipRequest.controller.js b/modules/organizations/controllers/organizations.membershipRequest.controller.js index 9e3a91138..53f57329e 100644 --- a/modules/organizations/controllers/organizations.membershipRequest.controller.js +++ b/modules/organizations/controllers/organizations.membershipRequest.controller.js @@ -4,6 +4,7 @@ import errors from '../../../lib/helpers/errors.js'; import responses from '../../../lib/helpers/responses.js'; import MembershipService from '../services/organizations.membership.service.js'; +import { MEMBERSHIP_ROLES, MEMBERSHIP_STATUSES } from '../lib/constants.js'; /** * @function create @@ -33,7 +34,7 @@ const create = async (req, res) => { */ const listPending = async (req, res) => { try { - if (!req.membership || req.membership.role === 'member') { + if (!req.membership || req.membership.role === MEMBERSHIP_ROLES.MEMBER) { return responses.success(res, 'membership request list')([]); } const requests = await MembershipService.listPending(req.organization._id || req.organization.id); @@ -165,7 +166,7 @@ const requestByID = async (req, res, next, id) => { const membership = await MembershipService.get(id); const organizationId = String(req.organization._id || req.organization.id); const membershipOrgId = String(membership?.organizationId?._id || membership?.organizationId); - if (!membership || membership.status !== 'pending' || membershipOrgId !== organizationId) { + if (!membership || membership.status !== MEMBERSHIP_STATUSES.PENDING || membershipOrgId !== organizationId) { return responses.error(res, 404, 'Not Found', 'No pending request with that identifier has been found')(); } req.membershipRequest = membership; diff --git a/modules/organizations/services/organizations.membership.service.js b/modules/organizations/services/organizations.membership.service.js index 0d6266c70..e7b4ecb6e 100644 --- a/modules/organizations/services/organizations.membership.service.js +++ b/modules/organizations/services/organizations.membership.service.js @@ -27,7 +27,7 @@ const validateLastOwnerProtection = async (organizationId) => { status: MEMBERSHIP_STATUSES.ACTIVE, }); if (ownerCount <= 1) { - throw new Error('Cannot remove the last owner of an organization'); + throw new Error('Organization must have at least one active owner'); } }; diff --git a/modules/users/services/users.service.js b/modules/users/services/users.service.js index e8bd6149a..d66ccd15e 100644 --- a/modules/users/services/users.service.js +++ b/modules/users/services/users.service.js @@ -8,26 +8,15 @@ import AuthService from '../../auth/services/auth.service.js'; import UserRepository from '../repositories/users.repository.js'; import MembershipService from '../../organizations/services/organizations.membership.service.js'; import OrganizationsCrudService from '../../organizations/services/organizations.crud.service.js'; - -/** - * @desc Remove sensitive data from user object, returning only whitelisted keys. - * @param {Object} user - Mongoose document or plain user object - * @param {Array} [conf] - Optional list of keys to pick. Defaults to config.whitelists.users.default. - * @return {Object|null} sanitized user object or null - */ -const removeSensitive = (user, conf) => { - if (!user || typeof user !== 'object') return null; - const keys = conf || config.whitelists.users.default; - const plain = typeof user.toJSON === 'function' ? user.toJSON() : user; - return _.pick(plain, keys); -}; +import { MEMBERSHIP_ROLES } from '../../organizations/lib/constants.js'; +import { removeSensitive } from '../utils/sanitizeUser.js'; /** * @desc Function to get all users in db * @param {String} search * @param {Int} page * @param {Int} perPage - * @return {Promise} users selected + * @returns {Promise} users selected */ const list = async (search, page, perPage) => { const result = await UserRepository.list(search, page || 0, perPage || 20); @@ -35,9 +24,9 @@ const list = async (search, page, perPage) => { }; /** - * @desc Function to ask repository to create a user (define provider, check & haspassword, save) + * @desc Function to ask repository to create a user (define provider, check & hashpassword, save) * @param {Object} user - * @return {Promise} user + * @returns {Promise} created user (sanitized) */ const create = async (user) => { // Set provider to local @@ -59,8 +48,8 @@ const create = async (user) => { /** * @desc Function to ask repository to search users by request - * @param {Object} mongoose input request - * @return {Array} users + * @param {Object} input - mongoose query input + * @returns {Promise} matching users (sanitized) */ const search = async (input) => { const result = await UserRepository.search(input); @@ -69,8 +58,8 @@ const search = async (input) => { /** * @desc Function to ask repository to get a user by id or email - * @param {Object} user.id / user.email - * @return {Object} user + * @param {Object} user - object with id or email field + * @returns {Promise} sanitized user or null */ const get = async (user) => { const result = await UserRepository.get(user); @@ -79,8 +68,8 @@ const get = async (user) => { /** * @desc Function to ask repository to get a user by id or email without filter data return (test & intern usage) - * @param {Object} user.id / user.email - * @return {Object} user + * @param {Object} user - object with id or email field + * @returns {Promise} full user document or null */ const getBrut = async (user) => { const result = await UserRepository.get(user); @@ -88,11 +77,11 @@ const getBrut = async (user) => { }; /** - * @desc Functio to ask repository to update a user - * @param {Object} user - original user - * @param {Object} body - user edited - * @param {boolean} admin - true if admin update - * @return {Promise} user - + * @desc Function to ask repository to update a user + * @param {Object} user - original user document + * @param {Object} body - fields to update + * @param {string} [option] - update mode: 'admin', 'recover', or undefined for user self-update + * @returns {Promise} updated user (sanitized) */ const update = async (user, body, option) => { if (!option) user = _.assignIn(user, removeSensitive(body, config.whitelists.users.update)); @@ -104,9 +93,9 @@ const update = async (user, body, option) => { }; /** - * @desc Functio to ask repository to sign terms for current user - * @param {Object} user - original user - * @return {Promise} user - + * @desc Function to ask repository to sign terms for current user + * @param {Object} user - original user document + * @returns {Promise} updated user (sanitized) */ const terms = async (user) => { user = _.assignIn(user, { terms: new Date() }); @@ -115,9 +104,9 @@ const terms = async (user) => { }; /** - * @desc Function to ask repository to a user from db by id or email - * @param {Object} user - * @return {Promise} result & id + * @desc Function to remove a user from db and clean up associated memberships/orgs + * @param {Object} user - user document with _id or id field + * @returns {Promise} deletion result */ const remove = async (user) => { const userId = user._id || user.id; @@ -126,9 +115,9 @@ const remove = async (user) => { const memberships = await MembershipService.listByUser(userId); for (const membership of memberships) { const orgId = membership.organizationId._id || membership.organizationId; - if (membership.role === 'owner') { + if (membership.role === MEMBERSHIP_ROLES.OWNER) { // Check if this user is the only owner of the org - const ownerCount = await MembershipService.count({ organizationId: orgId, role: 'owner' }); + const ownerCount = await MembershipService.count({ organizationId: orgId, role: MEMBERSHIP_ROLES.OWNER }); if (ownerCount <= 1) { // Sole owner — delete the entire org and its memberships // Clear currentOrganization for affected users @@ -148,7 +137,7 @@ const remove = async (user) => { /** * @desc Function to get all stats of db - * @return {Promise} All stats + * @returns {Promise} user statistics */ const stats = async () => { const result = await UserRepository.stats(); diff --git a/modules/users/utils/sanitizeUser.js b/modules/users/utils/sanitizeUser.js new file mode 100644 index 000000000..c80a3437e --- /dev/null +++ b/modules/users/utils/sanitizeUser.js @@ -0,0 +1,21 @@ +/** + * Module dependencies + */ +import _ from 'lodash'; + +import config from '../../../config/index.js'; + +/** + * @desc Remove sensitive data from user object, returning only whitelisted keys. + * @param {Object} user - Mongoose document or plain user object + * @param {Array} [conf] - Optional list of keys to pick. Defaults to config.whitelists.users.default. + * @returns {Object|null} sanitized user object or null + */ +const removeSensitive = (user, conf) => { + if (!user || typeof user !== 'object') return null; + const keys = conf || config.whitelists.users.default; + const plain = typeof user.toJSON === 'function' ? user.toJSON() : user; + return _.pick(plain, keys); +}; + +export { removeSensitive }; From efe64b115a37b71d8d31168fa5c5044f5cb7905d Mon Sep 17 00:00:00 2001 From: Pierre Brisorgueil Date: Mon, 6 Apr 2026 12:48:47 +0200 Subject: [PATCH 08/10] fix(organizations): fail-closed RBAC guards, active-owner count filter --- .../controllers/organizations.membership.controller.js | 10 +++++++--- modules/users/services/users.service.js | 4 ++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/modules/organizations/controllers/organizations.membership.controller.js b/modules/organizations/controllers/organizations.membership.controller.js index 367dee292..1e0c7f831 100644 --- a/modules/organizations/controllers/organizations.membership.controller.js +++ b/modules/organizations/controllers/organizations.membership.controller.js @@ -35,7 +35,7 @@ const list = async (req, res) => { const updateRole = async (req, res) => { try { // Belt-and-suspenders: only owners can change roles (CASL blocks admins via no 'update Membership') - if (req.membership && req.membership.role !== MEMBERSHIP_ROLES.OWNER) { + if (!req.membership || req.membership.role !== MEMBERSHIP_ROLES.OWNER) { return responses.error(res, 403, 'Forbidden', 'Only owners can change member roles')(); } const membership = await MembershipService.updateRole(req.membershipDoc, req.body.role); @@ -54,8 +54,12 @@ const updateRole = async (req, res) => { */ const remove = async (req, res) => { try { - // Admins can only remove members, not other admins or owners - if (req.membership && req.membership.role !== MEMBERSHIP_ROLES.OWNER && req.membershipDoc.role !== MEMBERSHIP_ROLES.MEMBER) { + // Only owners can remove anyone; admins can only remove members + const actorRole = req.membership?.role; + const targetRole = req.membershipDoc.role; + const canRemove = actorRole === MEMBERSHIP_ROLES.OWNER + || (actorRole === MEMBERSHIP_ROLES.ADMIN && targetRole === MEMBERSHIP_ROLES.MEMBER); + if (!canRemove) { return responses.error(res, 403, 'Forbidden', 'Only owners can remove admins or other owners')(); } const result = await MembershipService.remove(req.membershipDoc); diff --git a/modules/users/services/users.service.js b/modules/users/services/users.service.js index d66ccd15e..c1c54eaa3 100644 --- a/modules/users/services/users.service.js +++ b/modules/users/services/users.service.js @@ -8,7 +8,7 @@ import AuthService from '../../auth/services/auth.service.js'; import UserRepository from '../repositories/users.repository.js'; import MembershipService from '../../organizations/services/organizations.membership.service.js'; import OrganizationsCrudService from '../../organizations/services/organizations.crud.service.js'; -import { MEMBERSHIP_ROLES } from '../../organizations/lib/constants.js'; +import { MEMBERSHIP_ROLES, MEMBERSHIP_STATUSES } from '../../organizations/lib/constants.js'; import { removeSensitive } from '../utils/sanitizeUser.js'; /** @@ -117,7 +117,7 @@ const remove = async (user) => { const orgId = membership.organizationId._id || membership.organizationId; if (membership.role === MEMBERSHIP_ROLES.OWNER) { // Check if this user is the only owner of the org - const ownerCount = await MembershipService.count({ organizationId: orgId, role: MEMBERSHIP_ROLES.OWNER }); + const ownerCount = await MembershipService.count({ organizationId: orgId, role: MEMBERSHIP_ROLES.OWNER, status: MEMBERSHIP_STATUSES.ACTIVE }); if (ownerCount <= 1) { // Sole owner — delete the entire org and its memberships // Clear currentOrganization for affected users From 70b032047b09b6bcb92ca7e874f7320351eae581 Mon Sep 17 00:00:00 2001 From: Pierre Brisorgueil Date: Mon, 6 Apr 2026 12:54:08 +0200 Subject: [PATCH 09/10] test(organizations): add membership controller RBAC unit tests --- ...ations.membership.controller.unit.tests.js | 157 ++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 modules/organizations/tests/organizations.membership.controller.unit.tests.js diff --git a/modules/organizations/tests/organizations.membership.controller.unit.tests.js b/modules/organizations/tests/organizations.membership.controller.unit.tests.js new file mode 100644 index 000000000..01bca35cb --- /dev/null +++ b/modules/organizations/tests/organizations.membership.controller.unit.tests.js @@ -0,0 +1,157 @@ +/** + * Module dependencies. + */ +import { jest, describe, test, expect, beforeEach } from '@jest/globals'; + +const mockList = jest.fn(); +const mockUpdateRole = jest.fn(); +const mockRemove = jest.fn(); +const mockGet = jest.fn(); + +jest.unstable_mockModule('../services/organizations.membership.service.js', () => ({ + default: { + list: mockList, + updateRole: mockUpdateRole, + remove: mockRemove, + get: mockGet, + }, +})); + +const { default: membershipController } = await import('../controllers/organizations.membership.controller.js'); + +/** + * Unit tests for the membership controller RBAC guards. + */ +describe('Membership controller unit tests:', () => { + /** + * @desc Build a minimal Express-like req object + * @param {Object} overrides + * @returns {Object} mock request + */ + function mockReq(overrides = {}) { + return { + query: {}, + body: {}, + organization: { _id: 'org1' }, + membership: { role: 'owner' }, + membershipDoc: { id: 'mem1', role: 'member', organizationId: 'org1' }, + ...overrides, + }; + } + + /** + * @desc Build a minimal Express-like res object with spies + * @returns {Object} mock response + */ + function mockRes() { + const res = {}; + res.status = jest.fn().mockReturnValue(res); + res.json = jest.fn().mockReturnValue(res); + return res; + } + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('updateRole', () => { + test('should reject when req.membership is missing (fail closed)', async () => { + const req = mockReq({ membership: undefined }); + const res = mockRes(); + + await membershipController.updateRole(req, res); + + expect(res.status).toHaveBeenCalledWith(403); + expect(mockUpdateRole).not.toHaveBeenCalled(); + }); + + test('should reject when actor is not an owner', async () => { + const req = mockReq({ membership: { role: 'admin' } }); + const res = mockRes(); + + await membershipController.updateRole(req, res); + + expect(res.status).toHaveBeenCalledWith(403); + expect(mockUpdateRole).not.toHaveBeenCalled(); + }); + + test('should allow owner to update role', async () => { + const updatedMembership = { id: 'mem1', role: 'admin' }; + mockUpdateRole.mockResolvedValue(updatedMembership); + + const req = mockReq({ membership: { role: 'owner' }, body: { role: 'admin' } }); + const res = mockRes(); + + await membershipController.updateRole(req, res); + + expect(mockUpdateRole).toHaveBeenCalledTimes(1); + expect(res.status).not.toHaveBeenCalledWith(403); + }); + }); + + describe('remove', () => { + test('should reject when actor is a member (cannot remove anyone)', async () => { + const req = mockReq({ membership: { role: 'member' }, membershipDoc: { id: 'mem2', role: 'member' } }); + const res = mockRes(); + + await membershipController.remove(req, res); + + expect(res.status).toHaveBeenCalledWith(403); + expect(mockRemove).not.toHaveBeenCalled(); + }); + + test('should reject when req.membership is missing', async () => { + const req = mockReq({ membership: undefined, membershipDoc: { id: 'mem2', role: 'member' } }); + const res = mockRes(); + + await membershipController.remove(req, res); + + expect(res.status).toHaveBeenCalledWith(403); + expect(mockRemove).not.toHaveBeenCalled(); + }); + + test('should reject when admin tries to remove an owner', async () => { + const req = mockReq({ membership: { role: 'admin' }, membershipDoc: { id: 'mem2', role: 'owner' } }); + const res = mockRes(); + + await membershipController.remove(req, res); + + expect(res.status).toHaveBeenCalledWith(403); + expect(mockRemove).not.toHaveBeenCalled(); + }); + + test('should reject when admin tries to remove an admin', async () => { + const req = mockReq({ membership: { role: 'admin' }, membershipDoc: { id: 'mem2', role: 'admin' } }); + const res = mockRes(); + + await membershipController.remove(req, res); + + expect(res.status).toHaveBeenCalledWith(403); + expect(mockRemove).not.toHaveBeenCalled(); + }); + + test('should allow admin to remove a member', async () => { + mockRemove.mockResolvedValue({ success: true }); + + const req = mockReq({ membership: { role: 'admin' }, membershipDoc: { id: 'mem2', role: 'member' } }); + const res = mockRes(); + + await membershipController.remove(req, res); + + expect(mockRemove).toHaveBeenCalledTimes(1); + expect(res.status).not.toHaveBeenCalledWith(403); + }); + + test('should allow owner to remove any member', async () => { + mockRemove.mockResolvedValue({ success: true }); + + const req = mockReq({ membership: { role: 'owner' }, membershipDoc: { id: 'mem2', role: 'admin' } }); + const res = mockRes(); + + await membershipController.remove(req, res); + + expect(mockRemove).toHaveBeenCalledTimes(1); + expect(res.status).not.toHaveBeenCalledWith(403); + }); + }); +}); From 90761ee6386baffc44167c18397e507d2b65c5da Mon Sep 17 00:00:00 2001 From: Pierre Brisorgueil Date: Mon, 6 Apr 2026 13:04:49 +0200 Subject: [PATCH 10/10] fix(organizations): neutral error message, MEMBERSHIP_ROLES constants in tests --- .../organizations.membership.controller.js | 2 +- ...ations.membership.controller.unit.tests.js | 23 ++++++++++--------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/modules/organizations/controllers/organizations.membership.controller.js b/modules/organizations/controllers/organizations.membership.controller.js index 1e0c7f831..6ce4c548b 100644 --- a/modules/organizations/controllers/organizations.membership.controller.js +++ b/modules/organizations/controllers/organizations.membership.controller.js @@ -60,7 +60,7 @@ const remove = async (req, res) => { const canRemove = actorRole === MEMBERSHIP_ROLES.OWNER || (actorRole === MEMBERSHIP_ROLES.ADMIN && targetRole === MEMBERSHIP_ROLES.MEMBER); if (!canRemove) { - return responses.error(res, 403, 'Forbidden', 'Only owners can remove admins or other owners')(); + return responses.error(res, 403, 'Forbidden', 'Insufficient permissions to remove this member')(); } const result = await MembershipService.remove(req.membershipDoc); responses.success(res, 'membership deleted')({ id: req.membershipDoc.id, ...result }); diff --git a/modules/organizations/tests/organizations.membership.controller.unit.tests.js b/modules/organizations/tests/organizations.membership.controller.unit.tests.js index 01bca35cb..d658a462a 100644 --- a/modules/organizations/tests/organizations.membership.controller.unit.tests.js +++ b/modules/organizations/tests/organizations.membership.controller.unit.tests.js @@ -2,6 +2,7 @@ * Module dependencies. */ import { jest, describe, test, expect, beforeEach } from '@jest/globals'; +import { MEMBERSHIP_ROLES } from '../lib/constants.js'; const mockList = jest.fn(); const mockUpdateRole = jest.fn(); @@ -33,8 +34,8 @@ describe('Membership controller unit tests:', () => { query: {}, body: {}, organization: { _id: 'org1' }, - membership: { role: 'owner' }, - membershipDoc: { id: 'mem1', role: 'member', organizationId: 'org1' }, + membership: { role: MEMBERSHIP_ROLES.OWNER }, + membershipDoc: { id: 'mem1', role: MEMBERSHIP_ROLES.MEMBER, organizationId: 'org1' }, ...overrides, }; } @@ -66,7 +67,7 @@ describe('Membership controller unit tests:', () => { }); test('should reject when actor is not an owner', async () => { - const req = mockReq({ membership: { role: 'admin' } }); + const req = mockReq({ membership: { role: MEMBERSHIP_ROLES.ADMIN } }); const res = mockRes(); await membershipController.updateRole(req, res); @@ -76,10 +77,10 @@ describe('Membership controller unit tests:', () => { }); test('should allow owner to update role', async () => { - const updatedMembership = { id: 'mem1', role: 'admin' }; + const updatedMembership = { id: 'mem1', role: MEMBERSHIP_ROLES.ADMIN }; mockUpdateRole.mockResolvedValue(updatedMembership); - const req = mockReq({ membership: { role: 'owner' }, body: { role: 'admin' } }); + const req = mockReq({ membership: { role: MEMBERSHIP_ROLES.OWNER }, body: { role: MEMBERSHIP_ROLES.ADMIN } }); const res = mockRes(); await membershipController.updateRole(req, res); @@ -91,7 +92,7 @@ describe('Membership controller unit tests:', () => { describe('remove', () => { test('should reject when actor is a member (cannot remove anyone)', async () => { - const req = mockReq({ membership: { role: 'member' }, membershipDoc: { id: 'mem2', role: 'member' } }); + const req = mockReq({ membership: { role: MEMBERSHIP_ROLES.MEMBER }, membershipDoc: { id: 'mem2', role: MEMBERSHIP_ROLES.MEMBER } }); const res = mockRes(); await membershipController.remove(req, res); @@ -101,7 +102,7 @@ describe('Membership controller unit tests:', () => { }); test('should reject when req.membership is missing', async () => { - const req = mockReq({ membership: undefined, membershipDoc: { id: 'mem2', role: 'member' } }); + const req = mockReq({ membership: undefined, membershipDoc: { id: 'mem2', role: MEMBERSHIP_ROLES.MEMBER } }); const res = mockRes(); await membershipController.remove(req, res); @@ -111,7 +112,7 @@ describe('Membership controller unit tests:', () => { }); test('should reject when admin tries to remove an owner', async () => { - const req = mockReq({ membership: { role: 'admin' }, membershipDoc: { id: 'mem2', role: 'owner' } }); + const req = mockReq({ membership: { role: MEMBERSHIP_ROLES.ADMIN }, membershipDoc: { id: 'mem2', role: MEMBERSHIP_ROLES.OWNER } }); const res = mockRes(); await membershipController.remove(req, res); @@ -121,7 +122,7 @@ describe('Membership controller unit tests:', () => { }); test('should reject when admin tries to remove an admin', async () => { - const req = mockReq({ membership: { role: 'admin' }, membershipDoc: { id: 'mem2', role: 'admin' } }); + const req = mockReq({ membership: { role: MEMBERSHIP_ROLES.ADMIN }, membershipDoc: { id: 'mem2', role: MEMBERSHIP_ROLES.ADMIN } }); const res = mockRes(); await membershipController.remove(req, res); @@ -133,7 +134,7 @@ describe('Membership controller unit tests:', () => { test('should allow admin to remove a member', async () => { mockRemove.mockResolvedValue({ success: true }); - const req = mockReq({ membership: { role: 'admin' }, membershipDoc: { id: 'mem2', role: 'member' } }); + const req = mockReq({ membership: { role: MEMBERSHIP_ROLES.ADMIN }, membershipDoc: { id: 'mem2', role: MEMBERSHIP_ROLES.MEMBER } }); const res = mockRes(); await membershipController.remove(req, res); @@ -145,7 +146,7 @@ describe('Membership controller unit tests:', () => { test('should allow owner to remove any member', async () => { mockRemove.mockResolvedValue({ success: true }); - const req = mockReq({ membership: { role: 'owner' }, membershipDoc: { id: 'mem2', role: 'admin' } }); + const req = mockReq({ membership: { role: MEMBERSHIP_ROLES.OWNER }, membershipDoc: { id: 'mem2', role: MEMBERSHIP_ROLES.ADMIN } }); const res = mockRes(); await membershipController.remove(req, res);