diff --git a/package-lock.json b/package-lock.json index 842f20ca26..6768398f1b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1784,17 +1784,17 @@ } }, "node_modules/@cacheable/memory": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.0.7.tgz", - "integrity": "sha512-RbxnxAMf89Tp1dLhXMS7ceft/PGsDl1Ip7T20z5nZ+pwIAsQ1p2izPjVG69oCLv/jfQ7HDPHTWK0c9rcAWXN3A==", + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.0.8.tgz", + "integrity": "sha512-FvEb29x5wVwu/Kf93IWwsOOEuhHh6dYCJF3vcKLzXc0KXIW181AOzv6ceT4ZpBHDvAfG60eqb+ekmrnLHIy+jw==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@cacheable/utils": "^2.3.3", - "@keyv/bigmap": "^1.3.0", - "hookified": "^1.14.0", - "keyv": "^5.5.5" + "@cacheable/utils": "^2.4.0", + "@keyv/bigmap": "^1.3.1", + "hookified": "^1.15.1", + "keyv": "^5.6.0" } }, "node_modules/@cacheable/memory/node_modules/@keyv/bigmap": { @@ -1827,14 +1827,14 @@ } }, "node_modules/@cacheable/utils": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.3.4.tgz", - "integrity": "sha512-knwKUJEYgIfwShABS1BX6JyJJTglAFcEU7EXqzTdiGCXur4voqkiJkdgZIQtWNFhynzDWERcTYv/sETMu3uJWA==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.4.0.tgz", + "integrity": "sha512-PeMMsqjVq+bF0WBsxFBxr/WozBJiZKY0rUojuaCoIaKnEl3Ju1wfEwS+SV1DU/cSe8fqHIPiYJFif8T3MVt4cQ==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "hashery": "^1.3.0", + "hashery": "^1.5.0", "keyv": "^5.6.0" } }, @@ -2623,16 +2623,16 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", - "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", "dev": true, "license": "Apache-2.0", "peer": true, "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", - "minimatch": "^3.1.2" + "minimatch": "^3.1.5" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2666,21 +2666,21 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", - "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "ajv": "^6.12.4", + "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", - "minimatch": "^3.1.2", + "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" }, "engines": { @@ -2705,9 +2705,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", - "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", "dev": true, "license": "MIT", "peer": true, @@ -3137,15 +3137,15 @@ } }, "node_modules/@jsonjoy.com/fs-core": { - "version": "4.56.10", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-core/-/fs-core-4.56.10.tgz", - "integrity": "sha512-PyAEA/3cnHhsGcdY+AmIU+ZPqTuZkDhCXQ2wkXypdLitSpd6d5Ivxhnq4wa2ETRWFVJGabYynBWxIijOswSmOw==", + "version": "4.56.11", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-core/-/fs-core-4.56.11.tgz", + "integrity": "sha512-wThHjzUp01ImIjfCwhs+UnFkeGPFAymwLEkOtenHewaKe2pTP12p6r1UuwikA9NEvNf9Vlck92r8fb8n/MWM5w==", "dev": true, "license": "Apache-2.0", "peer": true, "dependencies": { - "@jsonjoy.com/fs-node-builtins": "4.56.10", - "@jsonjoy.com/fs-node-utils": "4.56.10", + "@jsonjoy.com/fs-node-builtins": "4.56.11", + "@jsonjoy.com/fs-node-utils": "4.56.11", "thingies": "^2.5.0" }, "engines": { @@ -3160,16 +3160,16 @@ } }, "node_modules/@jsonjoy.com/fs-fsa": { - "version": "4.56.10", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-fsa/-/fs-fsa-4.56.10.tgz", - "integrity": "sha512-/FVK63ysNzTPOnCCcPoPHt77TOmachdMS422txM4KhxddLdbW1fIbFMYH0AM0ow/YchCyS5gqEjKLNyv71j/5Q==", + "version": "4.56.11", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-fsa/-/fs-fsa-4.56.11.tgz", + "integrity": "sha512-ZYlF3XbMayyp97xEN8ZvYutU99PCHjM64mMZvnCseXkCJXJDVLAwlF8Q/7q/xiWQRsv3pQBj1WXHd9eEyYcaCQ==", "dev": true, "license": "Apache-2.0", "peer": true, "dependencies": { - "@jsonjoy.com/fs-core": "4.56.10", - "@jsonjoy.com/fs-node-builtins": "4.56.10", - "@jsonjoy.com/fs-node-utils": "4.56.10", + "@jsonjoy.com/fs-core": "4.56.11", + "@jsonjoy.com/fs-node-builtins": "4.56.11", + "@jsonjoy.com/fs-node-utils": "4.56.11", "thingies": "^2.5.0" }, "engines": { @@ -3184,18 +3184,18 @@ } }, "node_modules/@jsonjoy.com/fs-node": { - "version": "4.56.10", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node/-/fs-node-4.56.10.tgz", - "integrity": "sha512-7R4Gv3tkUdW3dXfXiOkqxkElxKNVdd8BDOWC0/dbERd0pXpPY+s2s1Mino+aTvkGrFPiY+mmVxA7zhskm4Ue4Q==", + "version": "4.56.11", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node/-/fs-node-4.56.11.tgz", + "integrity": "sha512-D65YrnP6wRuZyEWoSFnBJSr5zARVpVBGctnhie4rCsMuGXNzX7IHKaOt85/Aj7SSoG1N2+/xlNjWmkLvZ2H3Tg==", "dev": true, "license": "Apache-2.0", "peer": true, "dependencies": { - "@jsonjoy.com/fs-core": "4.56.10", - "@jsonjoy.com/fs-node-builtins": "4.56.10", - "@jsonjoy.com/fs-node-utils": "4.56.10", - "@jsonjoy.com/fs-print": "4.56.10", - "@jsonjoy.com/fs-snapshot": "4.56.10", + "@jsonjoy.com/fs-core": "4.56.11", + "@jsonjoy.com/fs-node-builtins": "4.56.11", + "@jsonjoy.com/fs-node-utils": "4.56.11", + "@jsonjoy.com/fs-print": "4.56.11", + "@jsonjoy.com/fs-snapshot": "4.56.11", "glob-to-regex.js": "^1.0.0", "thingies": "^2.5.0" }, @@ -3211,9 +3211,9 @@ } }, "node_modules/@jsonjoy.com/fs-node-builtins": { - "version": "4.56.10", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-builtins/-/fs-node-builtins-4.56.10.tgz", - "integrity": "sha512-uUnKz8R0YJyKq5jXpZtkGV9U0pJDt8hmYcLRrPjROheIfjMXsz82kXMgAA/qNg0wrZ1Kv+hrg7azqEZx6XZCVw==", + "version": "4.56.11", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-builtins/-/fs-node-builtins-4.56.11.tgz", + "integrity": "sha512-CNmt3a0zMCIhniFLXtzPWuUxXFU+U+2VyQiIrgt/rRVeEJNrMQUABaRbVxR0Ouw1LyR9RjaEkPM6nYpED+y43A==", "dev": true, "license": "Apache-2.0", "peer": true, @@ -3229,16 +3229,16 @@ } }, "node_modules/@jsonjoy.com/fs-node-to-fsa": { - "version": "4.56.10", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-to-fsa/-/fs-node-to-fsa-4.56.10.tgz", - "integrity": "sha512-oH+O6Y4lhn9NyG6aEoFwIBNKZeYy66toP5LJcDOMBgL99BKQMUf/zWJspdRhMdn/3hbzQsZ8EHHsuekbFLGUWw==", + "version": "4.56.11", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-to-fsa/-/fs-node-to-fsa-4.56.11.tgz", + "integrity": "sha512-5OzGdvJDgZVo+xXWEYo72u81zpOWlxlbG4d4nL+hSiW+LKlua/dldNgPrpWxtvhgyntmdFQad2UTxFyGjJAGhA==", "dev": true, "license": "Apache-2.0", "peer": true, "dependencies": { - "@jsonjoy.com/fs-fsa": "4.56.10", - "@jsonjoy.com/fs-node-builtins": "4.56.10", - "@jsonjoy.com/fs-node-utils": "4.56.10" + "@jsonjoy.com/fs-fsa": "4.56.11", + "@jsonjoy.com/fs-node-builtins": "4.56.11", + "@jsonjoy.com/fs-node-utils": "4.56.11" }, "engines": { "node": ">=10.0" @@ -3252,14 +3252,14 @@ } }, "node_modules/@jsonjoy.com/fs-node-utils": { - "version": "4.56.10", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-utils/-/fs-node-utils-4.56.10.tgz", - "integrity": "sha512-8EuPBgVI2aDPwFdaNQeNpHsyqPi3rr+85tMNG/lHvQLiVjzoZsvxA//Xd8aB567LUhy4QS03ptT+unkD/DIsNg==", + "version": "4.56.11", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-utils/-/fs-node-utils-4.56.11.tgz", + "integrity": "sha512-JADOZFDA3wRfsuxkT0+MYc4F9hJO2PYDaY66kRTG6NqGX3+bqmKu66YFYAbII/tEmQWPZeHoClUB23rtQM9UPg==", "dev": true, "license": "Apache-2.0", "peer": true, "dependencies": { - "@jsonjoy.com/fs-node-builtins": "4.56.10" + "@jsonjoy.com/fs-node-builtins": "4.56.11" }, "engines": { "node": ">=10.0" @@ -3273,14 +3273,14 @@ } }, "node_modules/@jsonjoy.com/fs-print": { - "version": "4.56.10", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-print/-/fs-print-4.56.10.tgz", - "integrity": "sha512-JW4fp5mAYepzFsSGrQ48ep8FXxpg4niFWHdF78wDrFGof7F3tKDJln72QFDEn/27M1yHd4v7sKHHVPh78aWcEw==", + "version": "4.56.11", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-print/-/fs-print-4.56.11.tgz", + "integrity": "sha512-rnaKRgCRIn8JGTjxhS0JPE38YM3Pj/H7SW4/tglhIPbfKEkky7dpPayNKV2qy25SZSL15oFVgH/62dMZ/z7cyA==", "dev": true, "license": "Apache-2.0", "peer": true, "dependencies": { - "@jsonjoy.com/fs-node-utils": "4.56.10", + "@jsonjoy.com/fs-node-utils": "4.56.11", "tree-dump": "^1.1.0" }, "engines": { @@ -3295,15 +3295,15 @@ } }, "node_modules/@jsonjoy.com/fs-snapshot": { - "version": "4.56.10", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-snapshot/-/fs-snapshot-4.56.10.tgz", - "integrity": "sha512-DkR6l5fj7+qj0+fVKm/OOXMGfDFCGXLfyHkORH3DF8hxkpDgIHbhf/DwncBMs2igu/ST7OEkexn1gIqoU6Y+9g==", + "version": "4.56.11", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-snapshot/-/fs-snapshot-4.56.11.tgz", + "integrity": "sha512-IIldPX+cIRQuUol9fQzSS3hqyECxVpYMJQMqdU3dCKZFRzEl1rkIkw4P6y7Oh493sI7YdxZlKr/yWdzEWZ1wGQ==", "dev": true, "license": "Apache-2.0", "peer": true, "dependencies": { "@jsonjoy.com/buffers": "^17.65.0", - "@jsonjoy.com/fs-node-utils": "4.56.10", + "@jsonjoy.com/fs-node-utils": "4.56.11", "@jsonjoy.com/json-pack": "^17.65.0", "@jsonjoy.com/util": "^17.65.0" }, @@ -5307,9 +5307,9 @@ } }, "node_modules/@types/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", "dev": true, "license": "MIT", "peer": true @@ -6459,9 +6459,9 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "peer": true, @@ -6644,9 +6644,9 @@ } }, "node_modules/asn1.js/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", "dev": true, "license": "MIT", "peer": true @@ -6805,9 +6805,9 @@ } }, "node_modules/babel-loader": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-10.0.0.tgz", - "integrity": "sha512-z8jt+EdS61AMw22nSfoNJAZ0vrtmhPRVi6ghL3rCeRZI8cdNYFiV5xeV3HbE7rlZZNmGH8BVccwWt8/ED0QOHA==", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-10.1.1.tgz", + "integrity": "sha512-JwKSzk2kjIe7mgPK+/lyZ2QAaJcpahNAdM+hgR2HI8D0OJVkdj8Rl6J3kaLYki9pwF7P2iWnD8qVv80Lq1ABtg==", "dev": true, "license": "MIT", "peer": true, @@ -6818,8 +6818,17 @@ "node": "^18.20.0 || ^20.10.0 || >=22.0.0" }, "peerDependencies": { - "@babel/core": "^7.12.0", + "@babel/core": "^7.12.0 || ^8.0.0-beta.1", + "@rspack/core": "^1.0.0 || ^2.0.0-0", "webpack": ">=5.61.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } } }, "node_modules/babel-loader-exclude-node-modules-except": { @@ -6985,9 +6994,9 @@ "license": "MIT" }, "node_modules/bn.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz", - "integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.3.tgz", + "integrity": "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==", "dev": true, "license": "MIT", "peer": true @@ -7377,17 +7386,17 @@ } }, "node_modules/cacheable": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.3.2.tgz", - "integrity": "sha512-w+ZuRNmex9c1TR9RcsxbfTKCjSL0rh1WA5SABbrWprIHeNBdmyQLSYonlDy9gpD+63XT8DgZ/wNh1Smvc9WnJA==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.3.3.tgz", + "integrity": "sha512-iffYMX4zxKp54evOH27fm92hs+DeC1DhXmNVN8Tr94M/iZIV42dqTHSR2Ik4TOSPyOAwKr7Yu3rN9ALoLkbWyQ==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@cacheable/memory": "^2.0.7", - "@cacheable/utils": "^2.3.3", + "@cacheable/memory": "^2.0.8", + "@cacheable/utils": "^2.4.0", "hookified": "^1.15.0", - "keyv": "^5.5.5", + "keyv": "^5.6.0", "qified": "^0.6.0" } }, @@ -7952,9 +7961,9 @@ "peer": true }, "node_modules/cosmiconfig": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", - "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz", + "integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==", "dev": true, "license": "MIT", "peer": true, @@ -7992,9 +8001,9 @@ } }, "node_modules/create-ecdh/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", "dev": true, "license": "MIT", "peer": true @@ -8519,9 +8528,9 @@ } }, "node_modules/diffie-hellman/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", "dev": true, "license": "MIT", "peer": true @@ -8754,9 +8763,9 @@ } }, "node_modules/elliptic/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", "dev": true, "license": "MIT", "peer": true @@ -8983,26 +8992,26 @@ } }, "node_modules/eslint": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", - "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.1", + "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.2", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "ajv": "^6.12.4", + "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", @@ -9021,7 +9030,7 @@ "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", + "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -9786,9 +9795,9 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz", + "integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==", "dev": true, "license": "ISC", "peer": true @@ -10333,9 +10342,9 @@ } }, "node_modules/hashery": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.4.0.tgz", - "integrity": "sha512-Wn2i1In6XFxl8Az55kkgnFRiAlIAushzh26PTjL2AKtQcEfXrcLa7Hn5QOWGZEf3LU057P9TwwZjFyxfS1VuvQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.0.tgz", + "integrity": "sha512-nhQ6ExaOIqti2FDWoEMWARUqIKyjr2VcZzXShrI+A3zpeiuPWzx6iPftt44LhP74E5sW36B75N6VHbvRtpvO6Q==", "dev": true, "license": "MIT", "peer": true, @@ -11202,9 +11211,9 @@ } }, "node_modules/is-network-error": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.0.tgz", - "integrity": "sha512-6oIwpsgRfnDiyEDLMay/GqCl3HoAtH5+RUKW29gYkL0QA+ipzpDLA16yQs7/RHCSu+BwgbJaOUqa4A99qNVQVw==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.1.tgz", + "integrity": "sha512-6QCxa49rQbmUWLfk0nuGqzql9U8uaV2H6279bRErPBHe/109hCzsLUBUHfbEtvLIHBd6hyXbgedBSHevm43Edw==", "dev": true, "license": "MIT", "peer": true, @@ -11659,9 +11668,9 @@ "peer": true }, "node_modules/launch-editor": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.13.0.tgz", - "integrity": "sha512-u+9asUHMJ99lA15VRMXw5XKfySFR9dGXwgsgS14YTbUq3GITP58mIM32At90P5fZ+MUId5Yw+IwI/yKub7jnCQ==", + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.13.1.tgz", + "integrity": "sha512-lPSddlAAluRKJ7/cjRFoXUFzaX7q/YKI7yPHuEvSJVqoXvFnJov1/Ud87Aa4zULIbA9Nja4mSPK8l0z/7eV2wA==", "dev": true, "license": "MIT", "peer": true, @@ -12168,21 +12177,21 @@ } }, "node_modules/memfs": { - "version": "4.56.10", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.56.10.tgz", - "integrity": "sha512-eLvzyrwqLHnLYalJP7YZ3wBe79MXktMdfQbvMrVD80K+NhrIukCVBvgP30zTJYEEDh9hZ/ep9z0KOdD7FSHo7w==", + "version": "4.56.11", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.56.11.tgz", + "integrity": "sha512-/GodtwVeKVIHZKLUSr2ZdOxKBC5hHki4JNCU22DoCGPEHr5o2PD5U721zvESKyWwCfTfavFl9WZYgA13OAYK0g==", "dev": true, "license": "Apache-2.0", "peer": true, "dependencies": { - "@jsonjoy.com/fs-core": "4.56.10", - "@jsonjoy.com/fs-fsa": "4.56.10", - "@jsonjoy.com/fs-node": "4.56.10", - "@jsonjoy.com/fs-node-builtins": "4.56.10", - "@jsonjoy.com/fs-node-to-fsa": "4.56.10", - "@jsonjoy.com/fs-node-utils": "4.56.10", - "@jsonjoy.com/fs-print": "4.56.10", - "@jsonjoy.com/fs-snapshot": "4.56.10", + "@jsonjoy.com/fs-core": "4.56.11", + "@jsonjoy.com/fs-fsa": "4.56.11", + "@jsonjoy.com/fs-node": "4.56.11", + "@jsonjoy.com/fs-node-builtins": "4.56.11", + "@jsonjoy.com/fs-node-to-fsa": "4.56.11", + "@jsonjoy.com/fs-node-utils": "4.56.11", + "@jsonjoy.com/fs-print": "4.56.11", + "@jsonjoy.com/fs-snapshot": "4.56.11", "@jsonjoy.com/json-pack": "^1.11.0", "@jsonjoy.com/util": "^1.9.0", "glob-to-regex.js": "^1.0.1", @@ -12199,9 +12208,9 @@ } }, "node_modules/meow": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-14.0.0.tgz", - "integrity": "sha512-JhC3R1f6dbspVtmF3vKjAWz1EVIvwFrGGPLSdU6rK79xBwHWTuHoLnRX/t1/zHS1Ch1Y2UtIrih7DAHuH9JFJA==", + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-14.1.0.tgz", + "integrity": "sha512-EDYo6VlmtnumlcBCbh1gLJ//9jvM/ndXHfVXIFrZVr6fGcwTUyCTFNTLCKuY3ffbK8L/+3Mzqnd58RojiZqHVw==", "dev": true, "license": "MIT", "peer": true, @@ -12736,9 +12745,9 @@ } }, "node_modules/miller-rabin/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", "dev": true, "license": "MIT", "peer": true @@ -14149,9 +14158,9 @@ } }, "node_modules/public-encrypt/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", "dev": true, "license": "MIT", "peer": true @@ -14905,15 +14914,15 @@ "peer": true }, "node_modules/sass": { - "version": "1.97.3", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.97.3.tgz", - "integrity": "sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==", + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.98.0.tgz", + "integrity": "sha512-+4N/u9dZ4PrgzGgPlKnaaRQx64RO0JBKs9sDhQ2pLgN6JQZ25uPQZKQYaBJU48Kd5BxgXoJ4e09Dq7nMcOUW3A==", "dev": true, "license": "MIT", "peer": true, "dependencies": { "chokidar": "^4.0.0", - "immutable": "^5.0.2", + "immutable": "^5.1.5", "source-map-js": ">=0.6.2 <2.0.0" }, "bin": { @@ -15980,9 +15989,9 @@ } }, "node_modules/stylelint": { - "version": "17.3.0", - "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-17.3.0.tgz", - "integrity": "sha512-1POV91lcEMhj6SLVaOeA0KlS9yattS+qq+cyWqP/nYzWco7K5jznpGH1ExngvPlTM9QF1Kjd2bmuzJu9TH2OcA==", + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-17.4.0.tgz", + "integrity": "sha512-3kQ2/cHv3Zt8OBg+h2B8XCx9evEABQIrv4hh3uXahGz/ZEHrTR80zxBiK2NfXNaSoyBzxO1pjsz1Vhdzwn5XSw==", "dev": true, "funding": [ { @@ -15999,15 +16008,14 @@ "dependencies": { "@csstools/css-calc": "^3.1.1", "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-syntax-patches-for-csstree": "^1.0.26", + "@csstools/css-syntax-patches-for-csstree": "^1.0.27", "@csstools/css-tokenizer": "^4.0.0", "@csstools/media-query-list-parser": "^5.0.0", "@csstools/selector-resolve-nested": "^4.0.0", "@csstools/selector-specificity": "^6.0.0", - "balanced-match": "^3.0.1", "colord": "^2.9.3", "cosmiconfig": "^9.0.0", - "css-functions-list": "^3.2.3", + "css-functions-list": "^3.3.3", "css-tree": "^3.1.0", "debug": "^4.4.3", "fast-glob": "^3.3.3", @@ -16021,7 +16029,6 @@ "import-meta-resolve": "^4.2.0", "imurmurhash": "^0.1.4", "is-plain-object": "^5.0.0", - "known-css-properties": "^0.37.0", "mathml-tag-names": "^4.0.0", "meow": "^14.0.0", "micromatch": "^4.0.8", @@ -16193,17 +16200,6 @@ "stylelint": ">= 11 < 18" } }, - "node_modules/stylelint/node_modules/balanced-match": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-3.0.1.tgz", - "integrity": "sha512-vjtV3hiLqYDNRoiAv0zC4QaGAMPomEoq83PRmYIofPswwZurCeWR5LByXm7SyoL0Zh5+2z0+HC7jG8gSZJUh0w==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 16" - } - }, "node_modules/stylelint/node_modules/file-entry-cache": { "version": "11.1.2", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.2.tgz", @@ -18545,14 +18541,13 @@ } }, "node_modules/write-file-atomic": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-7.0.0.tgz", - "integrity": "sha512-YnlPC6JqnZl6aO4uRc+dx5PHguiR9S6WeoLtpxNT9wIG+BDya7ZNE1q7KOjVgaA73hKhKLpVPgJ5QA9THQ5BRg==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-7.0.1.tgz", + "integrity": "sha512-OTIk8iR8/aCRWBqvxrzxR0hgxWpnYBblY1S5hDWBQfk/VFmJwzmJgQFN3WsoUKHISv2eAwe+PpbUzyL1CKTLXg==", "dev": true, "license": "ISC", "peer": true, "dependencies": { - "imurmurhash": "^0.1.4", "signal-exit": "^4.0.1" }, "engines": { diff --git a/src/components/AppNavigation/CalendarList.vue b/src/components/AppNavigation/CalendarList.vue index d88311a78e..9f1634778c 100644 --- a/src/components/AppNavigation/CalendarList.vue +++ b/src/components/AppNavigation/CalendarList.vue @@ -77,6 +77,14 @@ :calendar="calendar" /> + + + + + @@ -98,6 +106,7 @@ import CalendarListItemLoadingPlaceholder from './CalendarList/CalendarListItemL import CalendarListNew from './CalendarList/CalendarListNew.vue' import PublicCalendarListItem from './CalendarList/PublicCalendarListItem.vue' import useCalendarsStore from '../../store/calendars.js' +import useDelegationStore from '../../store/delegation.js' const limit = pLimit(1) @@ -129,12 +138,13 @@ export default { return { calendars: [], /** - * Calendars sorted by personal, shared, and deck + * Calendars sorted by personal, shared, deck, and delegated */ sortedCalendars: { personal: [], shared: [], deck: [], + delegated: [], }, disableDragging: false, @@ -143,7 +153,7 @@ export default { }, computed: { - ...mapStores(useCalendarsStore), + ...mapStores(useCalendarsStore, useDelegationStore), ...mapState(useCalendarsStore, { serverCalendars: 'sortedCalendarsSubscriptions', }), @@ -182,9 +192,15 @@ export default { personal: [], shared: [], deck: [], + delegated: [], } this.calendars.forEach((calendar) => { + if (calendar.isDelegated) { + this.sortedCalendars.delegated.push(calendar) + return + } + if (calendar.isSharedWithMe) { this.sortedCalendars.shared.push(calendar) return diff --git a/src/components/AppNavigation/Delegation.vue b/src/components/AppNavigation/Delegation.vue new file mode 100644 index 0000000000..98c0bafe2c --- /dev/null +++ b/src/components/AppNavigation/Delegation.vue @@ -0,0 +1,480 @@ + + + + + + + + + + + + + {{ t('calendar', 'Delegation') }} + + + + {{ t('calendar', 'Could not load delegates.') }} + + + + + + + + + + + + {{ t('calendar', 'Revoke access') }} + + + + + + + {{ t('calendar', 'No delegates yet.') }} + + + + + + + {{ t('calendar', 'Add delegate') }} + + + + + + + + + + + + + + {{ t('calendar', 'No users found') }} + + + + + + {{ user.displayname }} + {{ user.emailAddress }} + + + + + + {{ t('calendar', 'They will be able to manage your calendar events and invitations on your behalf.') }} + + + + + + + + + + diff --git a/src/models/calendar.js b/src/models/calendar.js index aaa34eeffa..dc9e9419da 100644 --- a/src/models/calendar.js +++ b/src/models/calendar.js @@ -45,6 +45,8 @@ function getDefaultCalendarObject(props = {}) { order: 0, // Whether or not the calendar is shared with me isSharedWithMe: false, + // Whether or not the calendar belongs to a user who delegated to me + isDelegated: false, // Whether or not the calendar can be shared by me canBeShared: false, // Whether or not the calendar can be published by me diff --git a/src/services/caldavService.js b/src/services/caldavService.js index ca80050452..447155be05 100644 --- a/src/services/caldavService.js +++ b/src/services/caldavService.js @@ -248,6 +248,42 @@ async function findPrincipalsInCollection(url, options = {}) { return getClient().findPrincipalsInCollection(url, options) } +/** + * Fetches all calendars from a calendar home at an arbitrary URL. + * Used to load calendars from another user's calendar home when acting as their proxy. + * + * This temporarily creates a CalendarHome object via the existing authenticated + * client so all credentials/headers are reused, then immediately restores the + * client's original calendar home list. + * + * NOTE: This relies on the cdav-library DavClient's `_extractCalendarHomes` method, + * which is a convention-private (underscore-prefixed) API. If the cdav-library + * changes this internal method, this function will need to be updated accordingly. + * + * @param {string} calendarHomeUrl Absolute URL of the calendar home to fetch from + * @return {Promise} Raw cdav-library Calendar objects + */ +async function findCalendarsAtUrl(calendarHomeUrl) { + const client = getClient() + const savedCalendarHomes = client.calendarHomes.slice() + + try { + // _extractCalendarHomes creates CalendarHome objects using the existing + // authenticated _request object and sets client.calendarHomes. + await client._extractCalendarHomes({ + '{urn:ietf:params:xml:ns:caldav}calendar-home-set': [calendarHomeUrl], + }) + + if (!client.calendarHomes.length) { + return [] + } + + return await client.calendarHomes[0].findAllCalendars() + } finally { + client.calendarHomes = savedCalendarHomes + } +} + export { advancedPrincipalPropertySearch, createCalendar, @@ -256,6 +292,7 @@ export { findAll, findAllCalendars, findAllDeletedCalendars, + findCalendarsAtUrl, findPrincipalByUrl, findPrincipalsInCollection, findPublicCalendarsByTokens, diff --git a/src/services/delegationService.js b/src/services/delegationService.js new file mode 100644 index 0000000000..598194f59d --- /dev/null +++ b/src/services/delegationService.js @@ -0,0 +1,249 @@ +/** + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +import axios from '@nextcloud/axios' +import { generateRemoteUrl } from '@nextcloud/router' +import logger from '../utils/logger.js' + +/** + * Returns the URL of a user's calendar proxy group. + * + * @param {string} userId The user ID (login name) + * @param {'read'|'write'} type Proxy group type + * @return {string} Absolute URL to the proxy group principal + */ +function getProxyGroupUrl(userId, type) { + return generateRemoteUrl(`dav/principals/users/${encodeURIComponent(userId)}/calendar-proxy-${type}`) +} + +/** + * Convert a potentially relative DAV href to an absolute URL. + * + * @param {string} href The href value from a DAV response + * @return {string} Absolute URL + */ +function toAbsoluteUrl(href) { + if (href.startsWith('http://') || href.startsWith('https://')) { + return href + } + return window.location.origin + href +} + +/** + * Parse href elements contained in a named DAV property element. + * + * @param {Document} doc Parsed XML document + * @param {string} propLocalName The local name of the DAV property (e.g. 'group-member-set') + * @return {string[]} List of absolute href strings + */ +function parseHrefsFromProp(doc, propLocalName) { + const propElements = doc.getElementsByTagNameNS('DAV:', propLocalName) + if (!propElements.length) { + return [] + } + const hrefElements = propElements[0].getElementsByTagNameNS('DAV:', 'href') + return Array.from(hrefElements).map((el) => toAbsoluteUrl(el.textContent.trim())) +} + +/** + * Perform a PROPFIND request and return the parsed response document. + * + * @param {string} url Request URL + * @param {string} body XML body + * @return {Promise} + */ +async function propfind(url, body) { + const response = await axios({ + method: 'PROPFIND', + url, + data: body, + headers: { + 'Content-Type': 'application/xml; charset=utf-8', + Depth: '0', + }, + }) + const parser = new DOMParser() + return parser.parseFromString(response.data, 'text/xml') +} + +/** + * Fetch the group-member-set of a principal collection (proxy group). + * + * @param {string} groupUrl Absolute URL of the proxy group principal + * @return {Promise} Absolute URLs of member principals + */ +async function fetchGroupMemberSet(groupUrl) { + const body = ` + + + + +` + + try { + const doc = await propfind(groupUrl, body) + return parseHrefsFromProp(doc, 'group-member-set') + } catch (error) { + logger.error('Could not fetch group-member-set', { groupUrl, error }) + return [] + } +} + +/** + * Fetch the group-membership list of a principal (groups it belongs to). + * + * @param {string} principalUrl Absolute URL of the user principal + * @return {Promise} Absolute URLs of groups the principal belongs to + */ +async function fetchGroupMembership(principalUrl) { + const body = ` + + + + +` + + try { + const doc = await propfind(principalUrl, body) + return parseHrefsFromProp(doc, 'group-membership') + } catch (error) { + logger.error('Could not fetch group-membership', { principalUrl, error }) + return [] + } +} + +/** + * Set the group-member-set of a proxy group via PROPPATCH. + * + * @param {string} groupUrl Absolute URL of the proxy group principal + * @param {string[]} memberUrls Absolute URLs of the new member set + * @return {Promise} + */ +async function setGroupMemberSet(groupUrl, memberUrls) { + const hrefs = memberUrls.map((url) => `${url}`).join('\n ') + const body = ` + + + + + ${hrefs} + + + +` + + await axios({ + method: 'PROPPATCH', + url: groupUrl, + data: body, + headers: { + 'Content-Type': 'application/xml; charset=utf-8', + }, + }) +} + +/** + * Get the absolute URLs of all write-delegates for a user. + * Delegates are principals in the user's calendar-proxy-write group. + * + * @param {string} userId The current user's login name + * @return {Promise} Absolute principal URLs of delegates + */ +async function getDelegateUrls(userId) { + const groupUrl = getProxyGroupUrl(userId, 'write') + return fetchGroupMemberSet(groupUrl) +} + +/** + * Add a delegate to the current user's calendar-proxy-write group. + * + * @param {string} userId The current user's login name + * @param {string} delegatePrincipalUrl Absolute URL of the principal to add + * @return {Promise} + */ +async function addDelegateToGroup(userId, delegatePrincipalUrl) { + const groupUrl = getProxyGroupUrl(userId, 'write') + const current = await fetchGroupMemberSet(groupUrl) + if (!current.includes(delegatePrincipalUrl)) { + await setGroupMemberSet(groupUrl, [...current, delegatePrincipalUrl]) + } +} + +/** + * Remove a delegate from the current user's calendar-proxy-write group. + * + * @param {string} userId The current user's login name + * @param {string} delegatePrincipalUrl Absolute URL of the principal to remove + * @return {Promise} + */ +async function removeDelegateFromGroup(userId, delegatePrincipalUrl) { + const groupUrl = getProxyGroupUrl(userId, 'write') + const current = await fetchGroupMemberSet(groupUrl) + await setGroupMemberSet(groupUrl, current.filter((url) => url !== delegatePrincipalUrl)) +} + +/** + * Get the principal URLs of users who have delegated write access to the current user. + * Inspects the group-membership property and strips the /calendar-proxy-write suffix to + * return the owner's principal URL directly, ready for CalDAV discovery. + * + * @param {string} principalUrl Absolute URL of the current user's principal + * @return {Promise} Absolute principal URLs of users who delegated to the current user + */ +async function getDelegatorPrincipalUrls(principalUrl) { + const groups = await fetchGroupMembership(principalUrl) + return groups + .filter((url) => url.includes('calendar-proxy-write')) + .map((url) => { + // URL pattern: .../principals/users/{userId}/calendar-proxy-write[/] + // Strip the proxy-group suffix to get the owner's principal URL. + const match = url.match(/^(.+\/principals\/users\/[^/]+)\/calendar-proxy-write/) + return match ? match[1] : null + }) + .filter(Boolean) +} + +/** + * Discover the calendar home URL for a principal via CalDAV PROPFIND. + * + * Performs a PROPFIND depth-0 on the principal URL requesting + * {urn:ietf:params:xml:ns:caldav}calendar-home-set, then returns the first href. + * This is the standards-correct (RFC 4791 §6.2.1) way to locate a user's calendar home + * and avoids hard-coding any URL path conventions. + * + * @param {string} principalUrl Absolute URL of the owner's principal + * @return {Promise} Absolute URL of the calendar home, or null on failure + */ +async function getCalendarHomeUrl(principalUrl) { + const body = ` + + + + +` + + try { + const doc = await propfind(principalUrl, body) + const propEl = doc.getElementsByTagNameNS('urn:ietf:params:xml:ns:caldav', 'calendar-home-set')[0] + if (!propEl) { + return null + } + const hrefs = Array.from(propEl.getElementsByTagNameNS('DAV:', 'href')) + .map((el) => toAbsoluteUrl(el.textContent.trim())) + .filter(Boolean) + return hrefs[0] ?? null + } catch (error) { + logger.error('Could not fetch calendar-home-set', { principalUrl, error }) + return null + } +} + +export { + addDelegateToGroup, + getCalendarHomeUrl, + getDelegateUrls, + getDelegatorPrincipalUrls, + getProxyGroupUrl, + removeDelegateFromGroup, +} diff --git a/src/store/delegation.js b/src/store/delegation.js new file mode 100644 index 0000000000..a0048c63e4 --- /dev/null +++ b/src/store/delegation.js @@ -0,0 +1,199 @@ +/** + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +import { showError } from '@nextcloud/dialogs' +import { translate as t } from '@nextcloud/l10n' +import { defineStore } from 'pinia' +import { mapDavCollectionToCalendar } from '../models/calendar.js' +import { mapDavToPrincipal } from '../models/principal.js' +import { findCalendarsAtUrl, findPrincipalByUrl } from '../services/caldavService.js' +import { + addDelegateToGroup, + getCalendarHomeUrl, + getDelegateUrls, + getDelegatorPrincipalUrls, + removeDelegateFromGroup, +} from '../services/delegationService.js' +import logger from '../utils/logger.js' +import useCalendarsStore from './calendars.js' +import usePrincipalsStore from './principals.js' + +export default defineStore('delegation', { + state: () => { + return { + /** + * List of principal objects that the current user has delegated to + * (members of the current user's calendar-proxy-write group). + * + * @type {object[]} + */ + delegates: [], + + /** + * Principal URLs of users who have granted the current user proxy access. + * Stored as full absolute principal URLs so calendar-home discovery can + * be performed without reconstructing paths from user IDs. + * + * @type {string[]} + */ + delegatorPrincipalUrls: [], + } + }, + + getters: { + /** + * Whether any delegated calendars exist (i.e. the current user is a delegate for someone). + * + * @param {object} state The store state + * @return {boolean} + */ + hasDelegatedCalendars: (state) => state.delegatorPrincipalUrls.length > 0, + }, + + actions: { + /** + * Fetch the current user's delegates (members of their calendar-proxy-write group) + * and resolve their principal details. + * + * @return {Promise} + */ + async fetchDelegates() { + const principalsStore = usePrincipalsStore() + const currentUser = principalsStore.getCurrentUserPrincipal + if (!currentUser?.userId) { + return + } + + let memberUrls + try { + memberUrls = await getDelegateUrls(currentUser.userId) + } catch (error) { + logger.error('Could not fetch delegate URLs', { error }) + return + } + + const delegates = [] + for (const url of memberUrls) { + try { + const dav = await findPrincipalByUrl(url) + if (dav) { + delegates.push(mapDavToPrincipal(dav)) + } + } catch (error) { + logger.error('Could not resolve delegate principal', { url, error }) + } + } + this.delegates = delegates + logger.debug('Fetched delegates', { delegates: this.delegates }) + }, + + /** + * Fetch the principal URLs of users who have granted the current user proxy access. + * + * @return {Promise} + */ + async fetchDelegators() { + const principalsStore = usePrincipalsStore() + const currentUser = principalsStore.getCurrentUserPrincipal + if (!currentUser?.url) { + return + } + + try { + this.delegatorPrincipalUrls = await getDelegatorPrincipalUrls(currentUser.url) + logger.debug('Fetched delegators', { delegatorPrincipalUrls: this.delegatorPrincipalUrls }) + } catch (error) { + logger.error('Could not fetch delegator principal URLs', { error }) + } + }, + + /** + * Add a user as a delegate (add them to the current user's calendar-proxy-write group). + * + * @param {object} data The destructuring object + * @param {string} data.principalUrl Absolute URL of the principal to add + * @return {Promise} + */ + async addDelegate({ principalUrl }) { + const principalsStore = usePrincipalsStore() + const currentUser = principalsStore.getCurrentUserPrincipal + if (!currentUser?.userId) { + return + } + + await addDelegateToGroup(currentUser.userId, principalUrl) + await this.fetchDelegates() + }, + + /** + * Remove a delegate (remove them from the current user's calendar-proxy-write group). + * + * @param {object} data The destructuring object + * @param {string} data.principalUrl Absolute URL of the principal to remove + * @return {Promise} + */ + async removeDelegate({ principalUrl }) { + const principalsStore = usePrincipalsStore() + const currentUser = principalsStore.getCurrentUserPrincipal + if (!currentUser?.userId) { + return + } + + await removeDelegateFromGroup(currentUser.userId, principalUrl) + this.delegates = this.delegates.filter((d) => d.url !== principalUrl) + }, + + /** + * Fetch all calendars from delegators' calendar homes and add them to the + * calendars store so they participate in normal event fetching and rendering. + * The calendars are tagged with isDelegated=true so CalendarList can show them + * in their own section. + * + * Calendar home URLs are discovered via CalDAV PROPFIND on each delegator's + * principal (RFC 4791 §6.2.1) rather than being constructed from user IDs, + * which ensures correctness regardless of server path conventions. + * + * @return {Promise} + */ + async fetchDelegatedCalendars() { + if (!this.delegatorPrincipalUrls.length) { + return + } + + const principalsStore = usePrincipalsStore() + const calendarsStore = useCalendarsStore() + const currentUser = principalsStore.getCurrentUserPrincipal + + for (const delegatorPrincipalUrl of this.delegatorPrincipalUrls) { + // Discover the delegator's calendar home URL via CalDAV principal PROPFIND. + // This follows RFC 4791 §6.2.1 and avoids hard-coding any URL path convention. + const calendarHomeUrl = await getCalendarHomeUrl(delegatorPrincipalUrl) + if (!calendarHomeUrl) { + logger.warn('Could not determine calendar home URL for delegator', { delegatorPrincipalUrl }) + showError(t('calendar', 'Could not load delegated calendars. Make sure the server supports calendar delegation.')) + continue + } + + try { + const rawCalendars = await findCalendarsAtUrl(calendarHomeUrl) + const mappedCalendars = rawCalendars + .map((cal) => mapDavCollectionToCalendar(cal, currentUser)) + .map((cal) => ({ ...cal, isDelegated: true })) + + for (const calendar of mappedCalendars) { + // Only add if not already present + if (!calendarsStore.getCalendarById(calendar.id)) { + calendarsStore.addCalendarMutation({ calendar }) + } + } + + logger.debug('Fetched delegated calendars from', { calendarHomeUrl, count: mappedCalendars.length }) + } catch (error) { + logger.error('Could not fetch calendars for delegator', { delegatorPrincipalUrl, error }) + showError(t('calendar', 'Could not load delegated calendars. Make sure the server supports calendar delegation.')) + } + } + }, + }, +}) diff --git a/src/views/Calendar.vue b/src/views/Calendar.vue index 81ef0c440f..65ee47c81d 100644 --- a/src/views/Calendar.vue +++ b/src/views/Calendar.vue @@ -43,6 +43,9 @@ + + + @@ -115,6 +118,7 @@ import AppNavigationHeader from '../components/AppNavigation/AppNavigationHeader import AppointmentConfigList from '../components/AppNavigation/AppointmentConfigList.vue' import CalendarList from '../components/AppNavigation/CalendarList.vue' import Trashbin from '../components/AppNavigation/CalendarList/Trashbin.vue' +import Delegation from '../components/AppNavigation/Delegation.vue' import EditCalendarModal from '../components/AppNavigation/EditCalendarModal.vue' import EmbedTopNavigation from '../components/AppNavigation/EmbedTopNavigation.vue' import ProposalList from '../components/AppNavigation/Proposal/ProposalList.vue' @@ -136,6 +140,7 @@ import { isNotifyPushAvailable, registerNotifyPushSyncListener } from '../servic import getTimezoneManager from '../services/timezoneDataProviderService.js' import useCalendarObjectsStore from '../store/calendarObjects.js' import useCalendarsStore from '../store/calendars.js' +import useDelegationStore from '../store/delegation.js' import useFetchedTimeRangesStore from '../store/fetchedTimeRanges.js' import usePrincipalsStore from '../store/principals.js' import useSettingsStore from '../store/settings.js' @@ -166,6 +171,7 @@ export default { NcContent, AppContent, AppNavigation, + Delegation, Trashbin, EditCalendarModal, EditSimple, @@ -222,6 +228,7 @@ export default { usePrincipalsStore, useSettingsStore, useWidgetStore, + useDelegationStore, ), ...mapState(useSettingsStore, { @@ -386,6 +393,10 @@ export default { }) } + // Load delegation info: who has delegated their calendars to the current user + await this.delegationStore.fetchDelegators() + await this.delegationStore.fetchDelegatedCalendars() + this.loadingCalendars = false } }, diff --git a/tests/javascript/unit/models/calendar.test.js b/tests/javascript/unit/models/calendar.test.js index 164747461b..6e60b63f2c 100644 --- a/tests/javascript/unit/models/calendar.test.js +++ b/tests/javascript/unit/models/calendar.test.js @@ -32,6 +32,7 @@ describe('Test suite: Calendar model (models/calendar.js)', () => { url: '', readOnly: false, order: 0, + isDelegated: false, isSharedWithMe: false, canBeShared: false, canBePublished: false, @@ -67,6 +68,7 @@ describe('Test suite: Calendar model (models/calendar.js)', () => { url: '', readOnly: false, order: 0, + isDelegated: false, isSharedWithMe: false, canBeShared: false, canBePublished: false, @@ -117,6 +119,7 @@ describe('Test suite: Calendar model (models/calendar.js)', () => { supportsEvents: true, supportsJournals: false, supportsTasks: false, + isDelegated: false, isSharedWithMe: false, canCreateObject: false, canDeleteObject: false, @@ -169,6 +172,7 @@ describe('Test suite: Calendar model (models/calendar.js)', () => { supportsEvents: true, supportsJournals: false, supportsTasks: false, + isDelegated: false, isSharedWithMe: false, canCreateObject: false, canDeleteObject: false, @@ -219,6 +223,7 @@ describe('Test suite: Calendar model (models/calendar.js)', () => { supportsEvents: true, supportsJournals: false, supportsTasks: false, + isDelegated: false, isSharedWithMe: false, canCreateObject: false, canDeleteObject: false, @@ -269,6 +274,7 @@ describe('Test suite: Calendar model (models/calendar.js)', () => { supportsEvents: true, supportsJournals: false, supportsTasks: false, + isDelegated: false, isSharedWithMe: true, canCreateObject: false, canDeleteObject: false, @@ -319,6 +325,7 @@ describe('Test suite: Calendar model (models/calendar.js)', () => { supportsEvents: true, supportsJournals: false, supportsTasks: false, + isDelegated: false, isSharedWithMe: false, canCreateObject: false, canDeleteObject: false, @@ -369,6 +376,7 @@ describe('Test suite: Calendar model (models/calendar.js)', () => { supportsEvents: true, supportsJournals: false, supportsTasks: false, + isDelegated: false, isSharedWithMe: false, canCreateObject: false, canDeleteObject: false, @@ -419,6 +427,7 @@ describe('Test suite: Calendar model (models/calendar.js)', () => { supportsEvents: true, supportsJournals: false, supportsTasks: false, + isDelegated: false, isSharedWithMe: false, canCreateObject: false, canDeleteObject: false, @@ -469,6 +478,7 @@ describe('Test suite: Calendar model (models/calendar.js)', () => { supportsEvents: true, supportsJournals: false, supportsTasks: false, + isDelegated: false, isSharedWithMe: false, canCreateObject: false, canDeleteObject: false, @@ -575,6 +585,7 @@ describe('Test suite: Calendar model (models/calendar.js)', () => { supportsEvents: true, supportsJournals: false, supportsTasks: false, + isDelegated: false, isSharedWithMe: false, canCreateObject: false, canDeleteObject: false, @@ -697,6 +708,7 @@ describe('Test suite: Calendar model (models/calendar.js)', () => { supportsEvents: true, supportsJournals: false, supportsTasks: false, + isDelegated: false, isSharedWithMe: true, canCreateObject: false, canDeleteObject: false, @@ -748,6 +760,7 @@ describe('Test suite: Calendar model (models/calendar.js)', () => { supportsEvents: true, supportsJournals: false, supportsTasks: false, + isDelegated: false, isSharedWithMe: false, canCreateObject: false, canDeleteObject: false,
+ {{ t('calendar', 'No delegates yet.') }} +
+ {{ t('calendar', 'They will be able to manage your calendar events and invitations on your behalf.') }} +