diff --git a/package.json b/package.json index 4bf8859..efb4d89 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "@base-ui/react": "^1.6.0", "@better-auth/passkey": "^1.5.5", "@dnd-kit/react": "^0.4.0", - "@polinetwork/backend": "^0.17.1", + "@polinetwork/backend": "^0.18.0", "@t3-oss/env-core": "^0.13.10", "@tanstack/react-router": "1.170.17", "@tanstack/react-start": "1.168.27", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 26ad80d..327c900 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,4 +1,4 @@ -lockfileVersion: "9.0" +lockfileVersion: '9.0' settings: autoInstallPeers: true @@ -16,6 +16,7 @@ overrides: '@oxlint/plugins': 1.79.0 importers: + .: dependencies: '@base-ui/react': @@ -28,8 +29,8 @@ importers: specifier: ^0.4.0 version: 0.4.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@polinetwork/backend': - specifier: ^0.17.1 - version: 0.17.1 + specifier: ^0.18.0 + version: 0.18.0 '@t3-oss/env-core': specifier: ^0.13.10 version: 0.13.11(typescript@6.0.3)(zod@4.3.5) @@ -140,12 +141,9 @@ packages: resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} - "@babel/code-frame@7.29.0": - resolution: - { - integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==, - } - engines: { node: ">=6.9.0" } + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} '@babel/code-frame@7.29.7': resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} @@ -155,19 +153,13 @@ packages: resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} engines: {node: '>=6.9.0'} - "@babel/core@7.29.0": - resolution: - { - integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==, - } - engines: { node: ">=6.9.0" } + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} - "@babel/generator@7.29.1": - resolution: - { - integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==, - } - engines: { node: ">=6.9.0" } + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} '@babel/generator@7.29.7': resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} @@ -177,25 +169,19 @@ packages: resolution: {integrity: sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==} engines: {node: '>=6.9.0'} - "@babel/helper-compilation-targets@7.28.6": - resolution: - { - integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==, - } - engines: { node: ">=6.9.0" } + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} '@babel/helper-create-class-features-plugin@7.29.7': resolution: {integrity: sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==} engines: {node: '>=6.9.0'} peerDependencies: - "@babel/core": ^7.0.0 + '@babel/core': ^7.0.0 - "@babel/helper-globals@7.28.0": - resolution: - { - integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==, - } - engines: { node: ">=6.9.0" } + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} '@babel/helper-globals@7.29.7': resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} @@ -205,12 +191,9 @@ packages: resolution: {integrity: sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==} engines: {node: '>=6.9.0'} - "@babel/helper-module-imports@7.28.6": - resolution: - { - integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==, - } - engines: { node: ">=6.9.0" } + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} '@babel/helper-module-imports@7.29.7': resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} @@ -220,7 +203,7 @@ packages: resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} engines: {node: '>=6.9.0'} peerDependencies: - "@babel/core": ^7.0.0 + '@babel/core': ^7.0.0 '@babel/helper-module-transforms@7.29.7': resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} @@ -240,29 +223,23 @@ packages: resolution: {integrity: sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==} engines: {node: '>=6.9.0'} peerDependencies: - "@babel/core": ^7.0.0 + '@babel/core': ^7.0.0 '@babel/helper-skip-transparent-expression-wrappers@7.29.7': resolution: {integrity: sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==} engines: {node: '>=6.9.0'} - "@babel/helper-string-parser@7.27.1": - resolution: - { - integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==, - } - engines: { node: ">=6.9.0" } + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} '@babel/helper-string-parser@7.29.7': resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} engines: {node: '>=6.9.0'} - "@babel/helper-validator-identifier@7.28.5": - resolution: - { - integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==, - } - engines: { node: ">=6.9.0" } + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} '@babel/helper-validator-identifier@7.29.7': resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} @@ -280,12 +257,9 @@ packages: resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} engines: {node: '>=6.9.0'} - "@babel/parser@7.29.2": - resolution: - { - integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==, - } - engines: { node: ">=6.0.0" } + '@babel/parser@7.29.2': + resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==} + engines: {node: '>=6.0.0'} hasBin: true '@babel/parser@7.29.7': @@ -297,42 +271,39 @@ packages: resolution: {integrity: sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==} engines: {node: '>=6.9.0'} peerDependencies: - "@babel/core": ^7.0.0-0 + '@babel/core': ^7.0.0-0 '@babel/plugin-syntax-typescript@7.29.7': resolution: {integrity: sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==} engines: {node: '>=6.9.0'} peerDependencies: - "@babel/core": ^7.0.0-0 + '@babel/core': ^7.0.0-0 '@babel/plugin-transform-modules-commonjs@7.29.7': resolution: {integrity: sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==} engines: {node: '>=6.9.0'} peerDependencies: - "@babel/core": ^7.0.0-0 + '@babel/core': ^7.0.0-0 '@babel/plugin-transform-typescript@7.29.7': resolution: {integrity: sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==} engines: {node: '>=6.9.0'} peerDependencies: - "@babel/core": ^7.0.0-0 + '@babel/core': ^7.0.0-0 '@babel/preset-typescript@7.29.7': resolution: {integrity: sha512-/Foi8vKY2EVbed/1eZx0gJEEwHAIxogrySI7rULcRIvhZzbvoE/b5qG5Ghc0WKAFKOHA9SD1x7RsFlOYdutIiQ==} engines: {node: '>=6.9.0'} peerDependencies: - "@babel/core": ^7.0.0-0 + '@babel/core': ^7.0.0-0 '@babel/runtime@7.29.7': resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} engines: {node: '>=6.9.0'} - "@babel/template@7.28.6": - resolution: - { - integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==, - } - engines: { node: ">=6.9.0" } + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} '@babel/template@7.29.7': resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} @@ -346,12 +317,9 @@ packages: resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} engines: {node: '>=6.9.0'} - "@babel/types@7.29.0": - resolution: - { - integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==, - } - engines: { node: ">=6.9.0" } + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} '@babel/types@7.29.7': resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} @@ -381,106 +349,82 @@ packages: '@base-ui/utils@0.3.1': resolution: {integrity: sha512-gFFiltORVmW/N6IILTGxizP3PBpVpysqML1ALY5Vk0mH+7faVkCknOU31goYHN5Aoek2dkjxva1XOD2Ce9WuIg==} peerDependencies: - "@types/react": ^17 || ^18 || ^19 + '@types/react': ^17 || ^18 || ^19 react: ^17 || ^18 || ^19 react-dom: ^17 || ^18 || ^19 peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@better-auth/core@1.5.5": - resolution: - { - integrity: sha512-1oR/2jAp821Dcf67kQYHUoyNcdc1TcShfw4QMK0YTVntuRES5mUOyvEJql5T6eIuLfaqaN4LOF78l0FtF66HXA==, - } + '@better-auth/core@1.5.5': + resolution: {integrity: sha512-1oR/2jAp821Dcf67kQYHUoyNcdc1TcShfw4QMK0YTVntuRES5mUOyvEJql5T6eIuLfaqaN4LOF78l0FtF66HXA==} peerDependencies: - "@better-auth/utils": 0.3.1 - "@better-fetch/fetch": 1.1.21 - "@cloudflare/workers-types": ">=4" + '@better-auth/utils': 0.3.1 + '@better-fetch/fetch': 1.1.21 + '@cloudflare/workers-types': '>=4' better-call: 1.3.2 jose: ^6.1.0 kysely: ^0.28.5 nanostores: ^1.0.1 peerDependenciesMeta: - "@cloudflare/workers-types": + '@cloudflare/workers-types': optional: true - "@better-auth/drizzle-adapter@1.5.5": - resolution: - { - integrity: sha512-HAi9xAP40oDt48QZeYBFTcmg3vt1Jik90GwoRIfangd7VGbxesIIDBJSnvwMbZ52GBIc6+V4FRw9lasNiNrPfw==, - } + '@better-auth/drizzle-adapter@1.5.5': + resolution: {integrity: sha512-HAi9xAP40oDt48QZeYBFTcmg3vt1Jik90GwoRIfangd7VGbxesIIDBJSnvwMbZ52GBIc6+V4FRw9lasNiNrPfw==} peerDependencies: - "@better-auth/core": 1.5.5 - "@better-auth/utils": ^0.3.0 - drizzle-orm: ">=0.41.0" + '@better-auth/core': 1.5.5 + '@better-auth/utils': ^0.3.0 + drizzle-orm: '>=0.41.0' peerDependenciesMeta: drizzle-orm: optional: true - "@better-auth/kysely-adapter@1.5.5": - resolution: - { - integrity: sha512-LmHffIVnqbfsxcxckMOoE8MwibWrbVFch+kwPKJ5OFDFv6lin75ufN7ZZ7twH0IMPLT/FcgzaRjP8jRrXRef9g==, - } + '@better-auth/kysely-adapter@1.5.5': + resolution: {integrity: sha512-LmHffIVnqbfsxcxckMOoE8MwibWrbVFch+kwPKJ5OFDFv6lin75ufN7ZZ7twH0IMPLT/FcgzaRjP8jRrXRef9g==} peerDependencies: - "@better-auth/core": 1.5.5 - "@better-auth/utils": ^0.3.0 + '@better-auth/core': 1.5.5 + '@better-auth/utils': ^0.3.0 kysely: ^0.27.0 || ^0.28.0 - "@better-auth/memory-adapter@1.5.5": - resolution: - { - integrity: sha512-4X0j1/2L+nsgmObjmy9xEGUFWUv38Qjthp558fwS3DAp6ueWWyCaxaD6VJZ7m5qPNMrsBStO5WGP8CmJTEWm7g==, - } + '@better-auth/memory-adapter@1.5.5': + resolution: {integrity: sha512-4X0j1/2L+nsgmObjmy9xEGUFWUv38Qjthp558fwS3DAp6ueWWyCaxaD6VJZ7m5qPNMrsBStO5WGP8CmJTEWm7g==} peerDependencies: - "@better-auth/core": 1.5.5 - "@better-auth/utils": ^0.3.0 + '@better-auth/core': 1.5.5 + '@better-auth/utils': ^0.3.0 - "@better-auth/mongo-adapter@1.5.5": - resolution: - { - integrity: sha512-P1J9ljL5X5k740I8Rx1esPWNgWYPdJR5hf2CY7BwDSrQFPUHuzeCg0YhtEEP55niNateTXhBqGAcy0fVOeamZg==, - } + '@better-auth/mongo-adapter@1.5.5': + resolution: {integrity: sha512-P1J9ljL5X5k740I8Rx1esPWNgWYPdJR5hf2CY7BwDSrQFPUHuzeCg0YhtEEP55niNateTXhBqGAcy0fVOeamZg==} peerDependencies: - "@better-auth/core": 1.5.5 - "@better-auth/utils": ^0.3.0 + '@better-auth/core': 1.5.5 + '@better-auth/utils': ^0.3.0 mongodb: ^6.0.0 || ^7.0.0 - "@better-auth/passkey@1.5.5": - resolution: - { - integrity: sha512-waGngvVophgoi/yqyU8fPZS04ZRMfjPBlxRlbV49nOgqFXcA9+914cGUedmaePXlTH6q6Z9K3dXUlg8H4g5tTQ==, - } + '@better-auth/passkey@1.5.5': + resolution: {integrity: sha512-waGngvVophgoi/yqyU8fPZS04ZRMfjPBlxRlbV49nOgqFXcA9+914cGUedmaePXlTH6q6Z9K3dXUlg8H4g5tTQ==} peerDependencies: - "@better-auth/core": 1.5.5 - "@better-auth/utils": 0.3.1 - "@better-fetch/fetch": 1.1.21 + '@better-auth/core': 1.5.5 + '@better-auth/utils': 0.3.1 + '@better-fetch/fetch': 1.1.21 better-auth: 1.5.5 better-call: 1.3.2 nanostores: ^1.0.1 - "@better-auth/prisma-adapter@1.5.5": - resolution: - { - integrity: sha512-CliDd78CXHzzwQIXhCdwGr5Ml53i6JdCHWV7PYwTIJz9EAm6qb2RVBdpP3nqEfNjINGM22A6gfleCgCdZkTIZg==, - } + '@better-auth/prisma-adapter@1.5.5': + resolution: {integrity: sha512-CliDd78CXHzzwQIXhCdwGr5Ml53i6JdCHWV7PYwTIJz9EAm6qb2RVBdpP3nqEfNjINGM22A6gfleCgCdZkTIZg==} peerDependencies: - "@better-auth/core": 1.5.5 - "@better-auth/utils": ^0.3.0 - "@prisma/client": ^5.0.0 || ^6.0.0 || ^7.0.0 + '@better-auth/core': 1.5.5 + '@better-auth/utils': ^0.3.0 + '@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0 prisma: ^5.0.0 || ^6.0.0 || ^7.0.0 peerDependenciesMeta: - "@prisma/client": + '@prisma/client': optional: true prisma: optional: true - "@better-auth/telemetry@1.5.5": - resolution: - { - integrity: sha512-1+lklxArn4IMHuU503RcPdXrSG2tlXt4jnGG3omolmspQ7tktg/Y9XO/yAkYDurtvMn1xJ8X1Ov01Ji/r5s9BQ==, - } + '@better-auth/telemetry@1.5.5': + resolution: {integrity: sha512-1+lklxArn4IMHuU503RcPdXrSG2tlXt4jnGG3omolmspQ7tktg/Y9XO/yAkYDurtvMn1xJ8X1Ov01Ji/r5s9BQ==} peerDependencies: '@better-auth/core': 1.5.5 @@ -607,14 +551,11 @@ packages: '@floating-ui/react-dom@2.1.8': resolution: {integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==} peerDependencies: - react: ">=16.8.0" - react-dom: ">=16.8.0" + react: '>=16.8.0' + react-dom: '>=16.8.0' - "@floating-ui/utils@0.2.11": - resolution: - { - integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==, - } + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} '@github/browserslist-config@1.0.0': resolution: {integrity: sha512-gIhjdJp/c2beaIWWIlsXdqXVRUz3r2BxBCpfz/F3JXHvSAQ1paMYjLH+maEATtENg+k5eLV7gA+9yPp762ieuw==} @@ -652,256 +593,166 @@ packages: resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} engines: {node: '>=18'} - "@img/sharp-darwin-arm64@0.34.5": - resolution: - { - integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==, - } - engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [darwin] - "@img/sharp-darwin-x64@0.34.5": - resolution: - { - integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==, - } - engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [darwin] - "@img/sharp-libvips-darwin-arm64@1.2.4": - resolution: - { - integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==, - } + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} cpu: [arm64] os: [darwin] - "@img/sharp-libvips-darwin-x64@1.2.4": - resolution: - { - integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==, - } + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} cpu: [x64] os: [darwin] - "@img/sharp-libvips-linux-arm64@1.2.4": - resolution: - { - integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==, - } + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] - "@img/sharp-libvips-linux-arm@1.2.4": - resolution: - { - integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==, - } + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] - "@img/sharp-libvips-linux-ppc64@1.2.4": - resolution: - { - integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==, - } + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] - "@img/sharp-libvips-linux-riscv64@1.2.4": - resolution: - { - integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==, - } + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} cpu: [riscv64] os: [linux] - "@img/sharp-libvips-linux-s390x@1.2.4": - resolution: - { - integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==, - } + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] - "@img/sharp-libvips-linux-x64@1.2.4": - resolution: - { - integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==, - } + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] - "@img/sharp-libvips-linuxmusl-arm64@1.2.4": - resolution: - { - integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==, - } + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] - "@img/sharp-libvips-linuxmusl-x64@1.2.4": - resolution: - { - integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==, - } + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] - "@img/sharp-linux-arm64@0.34.5": - resolution: - { - integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==, - } - engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - "@img/sharp-linux-arm@0.34.5": - resolution: - { - integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==, - } - engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] - "@img/sharp-linux-ppc64@0.34.5": - resolution: - { - integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==, - } - engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] - "@img/sharp-linux-riscv64@0.34.5": - resolution: - { - integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==, - } - engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [riscv64] os: [linux] - "@img/sharp-linux-s390x@0.34.5": - resolution: - { - integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==, - } - engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] - "@img/sharp-linux-x64@0.34.5": - resolution: - { - integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==, - } - engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - "@img/sharp-linuxmusl-arm64@0.34.5": - resolution: - { - integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==, - } - engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - "@img/sharp-linuxmusl-x64@0.34.5": - resolution: - { - integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==, - } - engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - "@img/sharp-wasm32@0.34.5": - resolution: - { - integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==, - } - engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [wasm32] - "@img/sharp-win32-arm64@0.34.5": - resolution: - { - integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==, - } - engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [win32] - "@img/sharp-win32-ia32@0.34.5": - resolution: - { - integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==, - } - engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ia32] os: [win32] - "@img/sharp-win32-x64@0.34.5": - resolution: - { - integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==, - } - engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [win32] '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} - "@jridgewell/remapping@2.3.5": - resolution: - { - integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==, - } - - "@jridgewell/resolve-uri@3.1.2": - resolution: - { - integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==, - } - engines: { node: ">=6.0.0" } - - "@jridgewell/sourcemap-codec@1.5.5": - resolution: - { - integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==, - } - - "@jridgewell/trace-mapping@0.3.31": - resolution: - { - integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==, - } - - "@levischuck/tiny-cbor@0.2.11": - resolution: - { - integrity: sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==, - } - - "@modelcontextprotocol/sdk@1.29.0": - resolution: - { - integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==, - } - engines: { node: ">=18" } - peerDependencies: - "@cfworker/json-schema": ^4.1.1 + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@levischuck/tiny-cbor@0.2.11': + resolution: {integrity: sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==} + + '@modelcontextprotocol/sdk@1.29.0': + resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 zod: ^3.25 || ^4.0 peerDependenciesMeta: - "@cfworker/json-schema": + '@cfworker/json-schema': optional: true '@mongodb-js/saslprep@1.4.13': @@ -913,81 +764,54 @@ packages: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 - "@next/env@15.5.18": - resolution: - { - integrity: sha512-hAV85Ckd9QR6RvH04MEKwsfLTksvFpO47j9xwtoIuvuPnlwecpSi+uZTtm8HirVbtlI2Fnz//xpcSTjFdyJk+g==, - } - - "@next/swc-darwin-arm64@15.5.18": - resolution: - { - integrity: sha512-w0WvQf1n+txiwns/9pwIQteCJpZTbxzO2SE0FLcwuD4v0WEh1JPOjdyxWL21XwJsdpx8cFRjyzxzCS/siP7HcQ==, - } - engines: { node: ">= 10" } + '@next/env@15.5.18': + resolution: {integrity: sha512-hAV85Ckd9QR6RvH04MEKwsfLTksvFpO47j9xwtoIuvuPnlwecpSi+uZTtm8HirVbtlI2Fnz//xpcSTjFdyJk+g==} + + '@next/swc-darwin-arm64@15.5.18': + resolution: {integrity: sha512-w0WvQf1n+txiwns/9pwIQteCJpZTbxzO2SE0FLcwuD4v0WEh1JPOjdyxWL21XwJsdpx8cFRjyzxzCS/siP7HcQ==} + engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - "@next/swc-darwin-x64@15.5.18": - resolution: - { - integrity: sha512-znn71QmDuxm+BOaglihMZfvyySMnNljkVIY5Z2TCssBmm+WqL6c19VhtH5ktFkHa8EZ2bnTUpcNcmNSQsg67og==, - } - engines: { node: ">= 10" } + '@next/swc-darwin-x64@15.5.18': + resolution: {integrity: sha512-znn71QmDuxm+BOaglihMZfvyySMnNljkVIY5Z2TCssBmm+WqL6c19VhtH5ktFkHa8EZ2bnTUpcNcmNSQsg67og==} + engines: {node: '>= 10'} cpu: [x64] os: [darwin] - "@next/swc-linux-arm64-gnu@15.5.18": - resolution: - { - integrity: sha512-yPPe5MNL+igZUa+OsqQJisqSfh6oarIuA1Q0BDxljGJhRQyZeP+WRHh7rs/jZUGMh5aY0YdIjXZG0VohkKkUdw==, - } - engines: { node: ">= 10" } + '@next/swc-linux-arm64-gnu@15.5.18': + resolution: {integrity: sha512-yPPe5MNL+igZUa+OsqQJisqSfh6oarIuA1Q0BDxljGJhRQyZeP+WRHh7rs/jZUGMh5aY0YdIjXZG0VohkKkUdw==} + engines: {node: '>= 10'} cpu: [arm64] os: [linux] - "@next/swc-linux-arm64-musl@15.5.18": - resolution: - { - integrity: sha512-glaCczEWIrHsokFZ3pP08U4BpKxwIdnT+txdOM32OBgpL9Yw4aqx8NejmgtZQZOdstQ5f0L3CasIZudzCuD+nw==, - } - engines: { node: ">= 10" } + '@next/swc-linux-arm64-musl@15.5.18': + resolution: {integrity: sha512-glaCczEWIrHsokFZ3pP08U4BpKxwIdnT+txdOM32OBgpL9Yw4aqx8NejmgtZQZOdstQ5f0L3CasIZudzCuD+nw==} + engines: {node: '>= 10'} cpu: [arm64] os: [linux] - "@next/swc-linux-x64-gnu@15.5.18": - resolution: - { - integrity: sha512-oUfg2EgJmU3R0OCOWiokGFUTvZiPfXtriXiuF3YNxRoROCdgvTedHIzYoeKH34gsZxS/V7mHbfq2hpAHwhH1/A==, - } - engines: { node: ">= 10" } + '@next/swc-linux-x64-gnu@15.5.18': + resolution: {integrity: sha512-oUfg2EgJmU3R0OCOWiokGFUTvZiPfXtriXiuF3YNxRoROCdgvTedHIzYoeKH34gsZxS/V7mHbfq2hpAHwhH1/A==} + engines: {node: '>= 10'} cpu: [x64] os: [linux] - "@next/swc-linux-x64-musl@15.5.18": - resolution: - { - integrity: sha512-JLxSP3KTd9iu/bvUMQxH7RJo9xKSHf55/6RPE4a6FTSZygGn7uvZbCej0AHXydwkggQGSD9UddSjwv6Xz5ESfA==, - } - engines: { node: ">= 10" } + '@next/swc-linux-x64-musl@15.5.18': + resolution: {integrity: sha512-JLxSP3KTd9iu/bvUMQxH7RJo9xKSHf55/6RPE4a6FTSZygGn7uvZbCej0AHXydwkggQGSD9UddSjwv6Xz5ESfA==} + engines: {node: '>= 10'} cpu: [x64] os: [linux] - "@next/swc-win32-arm64-msvc@15.5.18": - resolution: - { - integrity: sha512-ir1v7enP52K2HNz3tQQvwF+x7VNxBk1ciiZ18WBPvxf4C59IqdfmHPJYK3vH7rSxpuCVw/8C712wTXNAtEp+NA==, - } - engines: { node: ">= 10" } + '@next/swc-win32-arm64-msvc@15.5.18': + resolution: {integrity: sha512-ir1v7enP52K2HNz3tQQvwF+x7VNxBk1ciiZ18WBPvxf4C59IqdfmHPJYK3vH7rSxpuCVw/8C712wTXNAtEp+NA==} + engines: {node: '>= 10'} cpu: [arm64] os: [win32] - "@next/swc-win32-x64-msvc@15.5.18": - resolution: - { - integrity: sha512-LIu5me6QTANCd25E7I5uIEfvgQ06RK7tvHAbYo3zCb3VpxQEPvMcSpd87NwUABDT6MbGPdEGR5VRiK4PPTJhQg==, - } - engines: { node: ">= 10" } + '@next/swc-win32-x64-msvc@15.5.18': + resolution: {integrity: sha512-LIu5me6QTANCd25E7I5uIEfvgQ06RK7tvHAbYo3zCb3VpxQEPvMcSpd87NwUABDT6MbGPdEGR5VRiK4PPTJhQg==} + engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -999,26 +823,17 @@ packages: resolution: {integrity: sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==} engines: {node: '>= 20.19.0'} - "@nodelib/fs.scandir@2.1.5": - resolution: - { - integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==, - } - engines: { node: ">= 8" } - - "@nodelib/fs.stat@2.0.5": - resolution: - { - integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==, - } - engines: { node: ">= 8" } - - "@nodelib/fs.walk@1.2.8": - resolution: - { - integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==, - } - engines: { node: ">= 8" } + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} '@oozcitak/dom@2.0.2': resolution: {integrity: sha512-GjpKhkSYC3Mj4+lfwEyI1dqnsKTgwGy48ytZEhm4A/xnH/8z9M3ZVXKr/YGQi3uCLs1AEBS+x5T2JPiueEDW8w==} @@ -1463,8 +1278,8 @@ packages: resolution: {integrity: sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==} engines: {node: ^14.18.0 || >=16.0.0} - '@polinetwork/backend@0.17.1': - resolution: {integrity: sha512-Zhu2ar2DpxxV49Z+RDe6TdDwh8zuAUP2fhPXSStisCjNDou37L3Yk0K8Ggp6XuOIsvxlikCaLSqwtt8hpDY2lA==} + '@polinetwork/backend@0.18.0': + resolution: {integrity: sha512-B/4JNI9RB0bsPXDFHm0jkCLDEzqoydvZOz/G7bsR/9qti5cpFIUNaIbGPIRIjDGmVB5bO3DFVU6xM3iLc5eQ9A==} '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} @@ -1584,18 +1399,18 @@ packages: react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@types/react-dom": + '@types/react-dom': optional: true '@radix-ui/react-slot@1.3.0': resolution: {integrity: sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA==} peerDependencies: - "@types/react": "*" + '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true '@radix-ui/react-use-callback-ref@1.1.2': @@ -1604,16 +1419,16 @@ packages: '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true '@radix-ui/react-use-controllable-state@1.2.4': resolution: {integrity: sha512-cx2DixxmSfjCcEoRvDvy1NLd6SWK94XFcEEOZUcharUlXbmahFQGKCfwdKZL2ub34iIwOPOEFVF80xb+yfLYiA==} peerDependencies: - "@types/react": "*" + '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true '@radix-ui/react-use-effect-event@0.0.3': @@ -1622,7 +1437,7 @@ packages: '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true '@radix-ui/react-use-layout-effect@1.1.2': @@ -1631,7 +1446,7 @@ packages: '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true '@rolldown/binding-android-arm64@1.1.5': @@ -1763,7 +1578,7 @@ packages: resolution: {integrity: sha512-sM7GYY+KL7H/Hl0BE0inWfk3nRHZOLhmVn7sHGxaZt9FAR6KqREXAE+6TqKfiavfXmpRxO/OZ2QgKRd+oiBYRQ==} peerDependencies: arktype: ^2.1.0 - typescript: ">=5.0.0" + typescript: '>=5.0.0' valibot: ^1.0.0-beta.7 || ^1.0.0 zod: ^3.24.0 || ^4.0.0 peerDependenciesMeta: @@ -1838,11 +1653,11 @@ packages: engines: {node: '>=14.0.0'} cpu: [wasm32] bundledDependencies: - - "@napi-rs/wasm-runtime" - - "@emnapi/core" - - "@emnapi/runtime" - - "@tybys/wasm-util" - - "@emnapi/wasi-threads" + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' - tslib '@tailwindcss/oxide-win32-arm64-msvc@4.3.2': @@ -2048,19 +1863,13 @@ packages: '@trpc/server': 11.5.1 typescript: '>=5.7.2' - "@trpc/server@11.5.1": - resolution: - { - integrity: sha512-KIDzHRS5m8U1ncPwjgtOtPWK9lNO0kYL7b+lnvKXRqowSAQIEC/z6y7g/dkt4Aqv3DKI/STLydt2/afrP1QrxQ==, - } + '@trpc/server@11.5.1': + resolution: {integrity: sha512-KIDzHRS5m8U1ncPwjgtOtPWK9lNO0kYL7b+lnvKXRqowSAQIEC/z6y7g/dkt4Aqv3DKI/STLydt2/afrP1QrxQ==} peerDependencies: - typescript: ">=5.7.2" + typescript: '>=5.7.2' - "@ts-morph/common@0.27.0": - resolution: - { - integrity: sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ==, - } + '@ts-morph/common@0.27.0': + resolution: {integrity: sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ==} '@tybys/wasm-util@0.10.3': resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} @@ -2097,11 +1906,8 @@ packages: '@types/react@19.2.17': resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} - "@types/validate-npm-package-name@4.0.2": - resolution: - { - integrity: sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==, - } + '@types/validate-npm-package-name@4.0.2': + resolution: {integrity: sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==} '@types/webidl-conversions@7.0.3': resolution: {integrity: sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==} @@ -2447,11 +2253,8 @@ packages: oxlint-tsgolint: ^7.0.2001 accepts@2.0.0: - resolution: - { - integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==, - } - engines: { node: ">= 0.6" } + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} @@ -2472,10 +2275,7 @@ packages: optional: true ajv-formats@3.0.1: - resolution: - { - integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==, - } + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} peerDependencies: ajv: ^8.0.0 peerDependenciesMeta: @@ -2493,18 +2293,12 @@ packages: engines: {node: '>=6'} ansi-regex@5.0.1: - resolution: - { - integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==, - } - engines: { node: ">=8" } + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} ansi-regex@6.2.2: - resolution: - { - integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==, - } - engines: { node: ">=12" } + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} ansi-styles@5.2.0: resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} @@ -2515,10 +2309,7 @@ packages: engines: {node: '>=14'} argparse@2.0.1: - resolution: - { - integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==, - } + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} aria-hidden@1.2.6: resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} @@ -2556,11 +2347,8 @@ packages: engines: {node: '>= 0.4'} asn1js@3.0.7: - resolution: - { - integrity: sha512-uLvq6KJu04qoQM6gvBfKFjlh6Gl0vOKQuR5cJMDHQkmwfMOQeN3F3SHCv9SNYSL+CRoHvOGFfllDlVz03GQjvQ==, - } - engines: { node: ">=12.0.0" } + resolution: {integrity: sha512-uLvq6KJu04qoQM6gvBfKFjlh6Gl0vOKQuR5cJMDHQkmwfMOQeN3F3SHCv9SNYSL+CRoHvOGFfllDlVz03GQjvQ==} + engines: {node: '>=12.0.0'} assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} @@ -2597,43 +2385,31 @@ packages: resolution: {integrity: sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig==} babel-plugin-react-compiler@1.0.0: - resolution: - { - integrity: sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==, - } + resolution: {integrity: sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==} balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} balanced-match@4.0.4: - resolution: - { - integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==, - } - engines: { node: 18 || 20 || >=22 } + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} baseline-browser-mapping@2.10.16: - resolution: - { - integrity: sha512-Lyf3aK28zpsD1yQMiiHD4RvVb6UdMoo8xzG2XzFIfR9luPzOpcBlAsT/qfB1XWS1bxWT+UtE4WmQgsp297FYOA==, - } - engines: { node: ">=6.0.0" } + resolution: {integrity: sha512-Lyf3aK28zpsD1yQMiiHD4RvVb6UdMoo8xzG2XzFIfR9luPzOpcBlAsT/qfB1XWS1bxWT+UtE4WmQgsp297FYOA==} + engines: {node: '>=6.0.0'} hasBin: true better-auth@1.5.5: - resolution: - { - integrity: sha512-GpVPaV1eqr3mOovKfghJXXk6QvlcVeFbS3z+n+FPDid5rK/2PchnDtiaVCzWyXA9jH2KkirOfl+JhAUvnja0Eg==, - } - peerDependencies: - "@lynx-js/react": "*" - "@prisma/client": ^5.0.0 || ^6.0.0 || ^7.0.0 - "@sveltejs/kit": ^2.0.0 - "@tanstack/react-start": ^1.0.0 - "@tanstack/solid-start": ^1.0.0 + resolution: {integrity: sha512-GpVPaV1eqr3mOovKfghJXXk6QvlcVeFbS3z+n+FPDid5rK/2PchnDtiaVCzWyXA9jH2KkirOfl+JhAUvnja0Eg==} + peerDependencies: + '@lynx-js/react': '*' + '@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0 + '@sveltejs/kit': ^2.0.0 + '@tanstack/react-start': ^1.0.0 + '@tanstack/solid-start': ^1.0.0 better-sqlite3: ^12.0.0 - drizzle-kit: ">=0.31.4" - drizzle-orm: ">=0.41.0" + drizzle-kit: '>=0.31.4' + drizzle-orm: '>=0.41.0' mongodb: ^6.0.0 || ^7.0.0 mysql2: ^3.0.0 next: ^14.0.0 || ^15.0.0 || ^16.0.0 @@ -2646,15 +2422,15 @@ packages: vitest: ^2.0.0 || ^3.0.0 || ^4.0.0 vue: ^3.0.0 peerDependenciesMeta: - "@lynx-js/react": + '@lynx-js/react': optional: true - "@prisma/client": + '@prisma/client': optional: true - "@sveltejs/kit": + '@sveltejs/kit': optional: true - "@tanstack/react-start": + '@tanstack/react-start': optional: true - "@tanstack/solid-start": + '@tanstack/solid-start': optional: true better-sqlite3: optional: true @@ -2686,10 +2462,7 @@ packages: optional: true better-call@1.3.2: - resolution: - { - integrity: sha512-4cZIfrerDsNTn3cm+MhLbUePN0gdwkhSXEuG7r/zuQ8c/H7iU0/jSK5TD3FW7U0MgKHce/8jGpPYNO4Ve+4NBw==, - } + resolution: {integrity: sha512-4cZIfrerDsNTn3cm+MhLbUePN0gdwkhSXEuG7r/zuQ8c/H7iU0/jSK5TD3FW7U0MgKHce/8jGpPYNO4Ve+4NBw==} peerDependencies: zod: ^4.0.0 peerDependenciesMeta: @@ -2708,18 +2481,12 @@ packages: engines: {node: 18 || 20 || >=22} braces@3.0.3: - resolution: - { - integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==, - } - engines: { node: ">=8" } + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} browserslist@4.28.2: - resolution: - { - integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==, - } - engines: { node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7 } + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true bson@7.3.2: @@ -2731,43 +2498,28 @@ packages: engines: {node: '>=6'} bundle-name@4.1.0: - resolution: - { - integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==, - } - engines: { node: ">=18" } + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} bytes@3.1.2: - resolution: - { - integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==, - } - engines: { node: ">= 0.8" } + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} call-bind-apply-helpers@1.0.2: - resolution: - { - integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==, - } - engines: { node: ">= 0.4" } + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} call-bind@1.0.9: resolution: {integrity: sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==} engines: {node: '>= 0.4'} call-bound@1.0.4: - resolution: - { - integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==, - } - engines: { node: ">= 0.4" } + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} callsites@3.1.0: - resolution: - { - integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==, - } - engines: { node: ">=6" } + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} caniuse-lite@1.0.30001797: resolution: {integrity: sha512-l8xKG+gwAIExZGl9FrF7KUwuOmk6wbEPC9Xoy/RtnWv1XG0Q4LFlagaLpUv3Kiza3W/wm27zy0yWJEieYKAP6w==} @@ -2788,59 +2540,35 @@ packages: engines: {node: '>= 20.19.0'} class-variance-authority@0.7.1: - resolution: - { - integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==, - } + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} cli-cursor@5.0.0: - resolution: - { - integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==, - } - engines: { node: ">=18" } + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} cli-spinners@2.9.2: - resolution: - { - integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==, - } - engines: { node: ">=6" } + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} client-only@0.0.1: - resolution: - { - integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==, - } + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} clsx@2.1.1: - resolution: - { - integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==, - } - engines: { node: ">=6" } + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} cmdk@1.1.1: - resolution: - { - integrity: sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==, - } + resolution: {integrity: sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==} peerDependencies: react: ^18 || ^19 || ^19.0.0-rc react-dom: ^18 || ^19 || ^19.0.0-rc code-block-writer@13.0.3: - resolution: - { - integrity: sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==, - } + resolution: {integrity: sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==} commander@11.1.0: - resolution: - { - integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==, - } - engines: { node: ">=16" } + resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} + engines: {node: '>=16'} commander@14.0.3: resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} @@ -2868,11 +2596,8 @@ packages: engines: {node: ^14.18.0 || >=16.10.0} content-disposition@1.1.0: - resolution: - { - integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==, - } - engines: { node: ">=18" } + resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} + engines: {node: '>=18'} content-type@1.0.5: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} @@ -2889,38 +2614,26 @@ packages: resolution: {integrity: sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==} cookie-signature@1.2.2: - resolution: - { - integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==, - } - engines: { node: ">=6.6.0" } + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} cookie@0.7.2: - resolution: - { - integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==, - } - engines: { node: ">= 0.6" } + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} copy-anything@3.0.5: - resolution: - { - integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==, - } - engines: { node: ">=12.13" } + resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==} + engines: {node: '>=12.13'} cors@2.8.6: - resolution: - { - integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==, - } - engines: { node: ">= 0.10" } + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} cosmiconfig@9.0.2: resolution: {integrity: sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==} engines: {node: '>=14'} peerDependencies: - typescript: ">=4.9.5" + typescript: '>=4.9.5' peerDependenciesMeta: typescript: optional: true @@ -2938,11 +2651,8 @@ packages: optional: true cssesc@3.0.0: - resolution: - { - integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==, - } - engines: { node: ">=4" } + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} hasBin: true csstype@3.2.3: @@ -3002,22 +2712,16 @@ packages: optional: true debug@4.4.3: - resolution: - { - integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==, - } - engines: { node: ">=6.0" } + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} peerDependencies: - supports-color: "*" + supports-color: '*' peerDependenciesMeta: supports-color: optional: true dedent@1.7.2: - resolution: - { - integrity: sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==, - } + resolution: {integrity: sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==} peerDependencies: babel-plugin-macros: ^3.1.0 peerDependenciesMeta: @@ -3028,18 +2732,12 @@ packages: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} deepmerge@4.3.1: - resolution: - { - integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==, - } - engines: { node: ">=0.10.0" } + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} default-browser-id@5.0.1: - resolution: - { - integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==, - } - engines: { node: ">=18" } + resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} + engines: {node: '>=18'} default-browser@5.5.0: resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==} @@ -3054,52 +2752,34 @@ packages: engines: {node: '>=8'} define-lazy-prop@3.0.0: - resolution: - { - integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==, - } - engines: { node: ">=12" } + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} define-properties@1.2.1: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} defu@6.1.4: - resolution: - { - integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==, - } + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} depd@2.0.0: - resolution: - { - integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==, - } - engines: { node: ">= 0.8" } + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} detect-libc@2.1.2: - resolution: - { - integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==, - } - engines: { node: ">=8" } + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} detect-node-es@1.1.0: - resolution: - { - integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==, - } + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} diff@8.0.4: - resolution: - { - integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==, - } - engines: { node: ">=0.3.1" } + resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} + engines: {node: '>=0.3.1'} doctrine@2.1.0: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} @@ -3117,29 +2797,17 @@ packages: engines: {node: '>=12'} dunder-proto@1.0.1: - resolution: - { - integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==, - } - engines: { node: ">= 0.4" } + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} ee-first@1.1.1: - resolution: - { - integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==, - } + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} electron-to-chromium@1.5.334: - resolution: - { - integrity: sha512-mgjZAz7Jyx1SRCwEpy9wefDS7GvNPazLthHg8eQMJ76wBdGQQDW33TCrUTvQ4wzpmOrv2zrFoD3oNufMdyMpog==, - } + resolution: {integrity: sha512-mgjZAz7Jyx1SRCwEpy9wefDS7GvNPazLthHg8eQMJ76wBdGQQDW33TCrUTvQ4wzpmOrv2zrFoD3oNufMdyMpog==} emoji-regex@10.6.0: - resolution: - { - integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==, - } + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} @@ -3149,11 +2817,8 @@ packages: engines: {node: '>=14'} encodeurl@2.0.0: - resolution: - { - integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==, - } - engines: { node: ">= 0.8" } + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} enhanced-resolve@5.21.6: resolution: {integrity: sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==} @@ -3186,10 +2851,7 @@ packages: optional: true error-ex@1.3.4: - resolution: - { - integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==, - } + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} es-abstract-get@1.0.0: resolution: {integrity: sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==} @@ -3200,18 +2862,12 @@ packages: engines: {node: '>= 0.4'} es-define-property@1.0.1: - resolution: - { - integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==, - } - engines: { node: ">= 0.4" } + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} es-errors@1.3.0: - resolution: - { - integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==, - } - engines: { node: ">= 0.4" } + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} es-module-lexer@2.3.2: resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} @@ -3233,17 +2889,11 @@ packages: engines: {node: '>= 0.4'} escalade@3.2.0: - resolution: - { - integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==, - } - engines: { node: ">=6" } + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} escape-html@1.0.3: - resolution: - { - integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==, - } + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} escape-string-regexp@1.0.5: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} @@ -3448,11 +3098,8 @@ packages: engines: {node: ^20.19.0 || ^22.13.0 || >=24} esprima@4.0.1: - resolution: - { - integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==, - } - engines: { node: ">=4" } + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} hasBin: true esquery@1.7.0: @@ -3475,36 +3122,24 @@ packages: engines: {node: '>=0.10.0'} etag@1.8.1: - resolution: - { - integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==, - } - engines: { node: ">= 0.6" } + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} eventsource-parser@3.1.0: resolution: {integrity: sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==} engines: {node: '>=18.0.0'} eventsource@3.0.7: - resolution: - { - integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==, - } - engines: { node: ">=18.0.0" } + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} execa@5.1.1: - resolution: - { - integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==, - } - engines: { node: ">=10" } + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} execa@9.6.1: - resolution: - { - integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==, - } - engines: { node: ^18.19.0 || >=20.5.0 } + resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==} + engines: {node: ^18.19.0 || >=20.5.0} expect-type@1.4.0: resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} @@ -3514,7 +3149,7 @@ packages: resolution: {integrity: sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==} engines: {node: '>= 16'} peerDependencies: - express: ">= 4.11" + express: '>= 4.11' express@5.2.1: resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} @@ -3524,20 +3159,14 @@ packages: resolution: {integrity: sha512-D+42+T12DdIlJM3uepa55qGiL3sYdLBOxIl2ifQCzCHz4c7eiolaHsi3BIqEr7JxBzxv2pYZQX9kw16ziMcEmw==} fast-deep-equal@3.1.3: - resolution: - { - integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==, - } + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} fast-diff@1.3.0: resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} fast-glob@3.3.3: - resolution: - { - integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==, - } - engines: { node: ">=8.6.0" } + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} @@ -3549,17 +3178,11 @@ packages: resolution: {integrity: sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==} fastq@1.20.1: - resolution: - { - integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==, - } + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} fdir@6.5.0: - resolution: - { - integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==, - } - engines: { node: ">=12.0.0" } + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} peerDependencies: picomatch: ^3 || ^4 peerDependenciesMeta: @@ -3570,29 +3193,20 @@ packages: resolution: {integrity: sha512-YoZjBdafyLIop9lSxXVI33oLD5kN31q4Td+CasofLLYeLXRFeOsuOw0Uo+XNRi9PZlbfdlN2GmRtm4tCEQ9/KA==} figures@6.1.0: - resolution: - { - integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==, - } - engines: { node: ">=18" } + resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} + engines: {node: '>=18'} file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} fill-range@7.1.1: - resolution: - { - integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==, - } - engines: { node: ">=8" } + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} finalhandler@2.1.1: - resolution: - { - integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==, - } - engines: { node: ">= 18.0.0" } + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} find-up@3.0.0: resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==} @@ -3632,11 +3246,8 @@ packages: optional: true fresh@2.0.0: - resolution: - { - integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==, - } - engines: { node: ">= 0.8" } + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} fs-extra@11.3.6: resolution: {integrity: sha512-w8ZNZr2mKIc7qeNaQ9AVPT1+iFaI+Avd4xudVOvdDJ8VytREi1Ft5Ih7hd9jjehod8vAM5GMsfQ/TpPf4EyoEA==} @@ -3648,10 +3259,7 @@ packages: os: [darwin] function-bind@1.1.2: - resolution: - { - integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==, - } + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} function.prototype.name@1.2.0: resolution: {integrity: sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==} @@ -3664,78 +3272,51 @@ packages: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} fuzzysort@3.1.0: - resolution: - { - integrity: sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ==, - } + resolution: {integrity: sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ==} generator-function@2.0.1: resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} engines: {node: '>= 0.4'} gensync@1.0.0-beta.2: - resolution: - { - integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==, - } - engines: { node: ">=6.9.0" } + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} get-east-asian-width@1.6.0: resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} engines: {node: '>=18'} get-intrinsic@1.3.0: - resolution: - { - integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==, - } - engines: { node: ">= 0.4" } + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} get-nonce@1.0.1: - resolution: - { - integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==, - } - engines: { node: ">=6" } + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} get-own-enumerable-keys@1.0.0: - resolution: - { - integrity: sha512-PKsK2FSrQCyxcGHsGrLDcK0lx+0Ke+6e8KFFozA9/fIQLhQzPaRvJFdcz7+Axg3jUH/Mq+NI4xa5u/UT2tQskA==, - } - engines: { node: ">=14.16" } + resolution: {integrity: sha512-PKsK2FSrQCyxcGHsGrLDcK0lx+0Ke+6e8KFFozA9/fIQLhQzPaRvJFdcz7+Axg3jUH/Mq+NI4xa5u/UT2tQskA==} + engines: {node: '>=14.16'} get-proto@1.0.1: - resolution: - { - integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==, - } - engines: { node: ">= 0.4" } + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} get-stream@6.0.1: - resolution: - { - integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==, - } - engines: { node: ">=10" } + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} get-stream@9.0.1: - resolution: - { - integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==, - } - engines: { node: ">=18" } + resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} + engines: {node: '>=18'} get-symbol-description@1.1.0: resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} engines: {node: '>= 0.4'} glob-parent@5.1.2: - resolution: - { - integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==, - } - engines: { node: ">= 6" } + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} glob-parent@6.0.2: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} @@ -3754,17 +3335,11 @@ packages: engines: {node: '>= 0.4'} gopd@1.2.0: - resolution: - { - integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==, - } - engines: { node: ">= 0.4" } + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} graceful-fs@4.2.11: - resolution: - { - integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==, - } + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} h3@2.0.1-rc.20: resolution: {integrity: sha512-28ljodXuUp0fZovdiSRq4G9OgrxCztrJe5VdYzXAB7ueRvI7pIUqLU14Xi3XqdYJ/khXjfpUOOD2EQa6CmBgsg==} @@ -3798,11 +3373,8 @@ packages: engines: {node: '>= 0.4'} has-symbols@1.1.0: - resolution: - { - integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==, - } - engines: { node: ">= 0.4" } + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} has-tostringtag@1.0.2: resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} @@ -3824,60 +3396,42 @@ packages: engines: {node: '>=8'} http-errors@2.0.1: - resolution: - { - integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==, - } - engines: { node: ">= 0.8" } + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} httpxy@0.5.5: resolution: {integrity: sha512-uDjmnPyp1q4Sgzf3w+J/Fc6UqcCEj0x4Wjp7OqK5dGhNeDgpyrAmnS6ey8QWrX3SWDon2DMKf9sBa5X9+CVyMA==} human-signals@2.1.0: - resolution: - { - integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==, - } - engines: { node: ">=10.17.0" } + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} human-signals@8.0.1: - resolution: - { - integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==, - } - engines: { node: ">=18.18.0" } + resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} + engines: {node: '>=18.18.0'} iconv-lite@0.7.3: resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} engines: {node: '>=0.10.0'} ignore@5.3.2: - resolution: - { - integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==, - } - engines: { node: ">= 4" } + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} ignore@7.0.6: resolution: {integrity: sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==} engines: {node: '>= 4'} import-fresh@3.3.1: - resolution: - { - integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==, - } - engines: { node: ">=6" } + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} inherits@2.0.4: - resolution: - { - integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==, - } + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} inline-style-parser@0.2.7: resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} @@ -3891,11 +3445,8 @@ packages: engines: {node: '>= 12'} ipaddr.js@1.9.1: - resolution: - { - integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==, - } - engines: { node: ">= 0.10" } + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} is-array-buffer@3.0.5: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} @@ -3938,11 +3489,8 @@ packages: hasBin: true is-docker@3.0.0: - resolution: - { - integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==, - } - engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 } + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} hasBin: true is-document.all@1.0.0: @@ -3950,11 +3498,8 @@ packages: engines: {node: '>= 0.4'} is-extglob@2.1.1: - resolution: - { - integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==, - } - engines: { node: ">=0.10.0" } + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} is-finalizationregistry@1.1.1: resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} @@ -3965,37 +3510,25 @@ packages: engines: {node: '>= 0.4'} is-glob@4.0.3: - resolution: - { - integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==, - } - engines: { node: ">=0.10.0" } + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} is-html@2.0.0: resolution: {integrity: sha512-S+OpgB5i7wzIue/YSE5hg0e5ZYfG3hhpNh9KGl6ayJ38p7ED6wxQLd1TV91xHpcTvw90KMJ9EwN3F/iNflHBVg==} engines: {node: '>=8'} is-in-ssh@1.0.0: - resolution: - { - integrity: sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==, - } - engines: { node: ">=20" } + resolution: {integrity: sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==} + engines: {node: '>=20'} is-inside-container@1.0.0: - resolution: - { - integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==, - } - engines: { node: ">=14.16" } + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} hasBin: true is-interactive@2.0.0: - resolution: - { - integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==, - } - engines: { node: ">=12" } + resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} + engines: {node: '>=12'} is-map@2.0.3: resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} @@ -4018,35 +3551,23 @@ packages: engines: {node: '>=8'} is-obj@3.0.0: - resolution: - { - integrity: sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ==, - } - engines: { node: ">=12" } + resolution: {integrity: sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ==} + engines: {node: '>=12'} is-plain-obj@4.1.0: - resolution: - { - integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==, - } - engines: { node: ">=12" } + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} is-promise@4.0.0: - resolution: - { - integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==, - } + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} is-regexp@3.1.0: - resolution: - { - integrity: sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==, - } - engines: { node: ">=12" } + resolution: {integrity: sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==} + engines: {node: '>=12'} is-set@2.0.3: resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} @@ -4057,18 +3578,12 @@ packages: engines: {node: '>= 0.4'} is-stream@2.0.1: - resolution: - { - integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==, - } - engines: { node: ">=8" } + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} is-stream@4.0.1: - resolution: - { - integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==, - } - engines: { node: ">=18" } + resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} + engines: {node: '>=18'} is-string@1.1.1: resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} @@ -4083,18 +3598,12 @@ packages: engines: {node: '>= 0.4'} is-unicode-supported@1.3.0: - resolution: - { - integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==, - } - engines: { node: ">=12" } + resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} + engines: {node: '>=12'} is-unicode-supported@2.1.0: - resolution: - { - integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==, - } - engines: { node: ">=18" } + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} is-weakmap@2.0.2: resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} @@ -4128,39 +3637,24 @@ packages: engines: {node: '>=18'} isexe@2.0.0: - resolution: - { - integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==, - } + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} isexe@3.1.5: - resolution: - { - integrity: sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==, - } - engines: { node: ">=18" } + resolution: {integrity: sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==} + engines: {node: '>=18'} jiti@2.7.0: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true jose@6.1.3: - resolution: - { - integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==, - } + resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==} js-tokens@4.0.0: - resolution: - { - integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==, - } + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} js-yaml@4.1.1: - resolution: - { - integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==, - } + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true js-yaml@4.3.1: @@ -4172,21 +3666,15 @@ packages: engines: {node: ^22.22.2 || >=24.15.0} jsesc@3.1.0: - resolution: - { - integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==, - } - engines: { node: ">=6" } + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} hasBin: true json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} json-parse-even-better-errors@2.3.1: - resolution: - { - integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==, - } + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -4198,10 +3686,7 @@ packages: resolution: {integrity: sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A==} json-schema-typed@8.0.2: - resolution: - { - integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==, - } + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -4211,11 +3696,8 @@ packages: hasBin: true json5@2.2.3: - resolution: - { - integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==, - } - engines: { node: ">=6" } + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} hasBin: true jsonfile@6.2.1: @@ -4236,28 +3718,19 @@ packages: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} kleur@3.0.3: - resolution: - { - integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==, - } - engines: { node: ">=6" } + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} kleur@4.1.5: - resolution: - { - integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==, - } - engines: { node: ">=6" } + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} known-css-properties@0.30.0: resolution: {integrity: sha512-VSWXYUnsPu9+WYKkfmJyLKtIvaRJi1kXUqVmBACORXZQxT5oZDsoZ2vQP+bQFDnWtpI/4eq3MLoRMjI2fnLzTQ==} kysely@0.28.12: - resolution: - { - integrity: sha512-kWiueDWXhbCchgiotwXkwdxZE/6h56IHAeFWg4euUfW0YsmO9sxbAxzx1KLLv2lox15EfuuxHQvgJ1qIfZuHGw==, - } - engines: { node: ">=20.0.0" } + resolution: {integrity: sha512-kWiueDWXhbCchgiotwXkwdxZE/6h56IHAeFWg4euUfW0YsmO9sxbAxzx1KLLv2lox15EfuuxHQvgJ1qIfZuHGw==} + engines: {node: '>=20.0.0'} language-subtag-registry@0.3.23: resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} @@ -4441,11 +3914,8 @@ packages: resolution: {integrity: sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==} log-symbols@6.0.0: - resolution: - { - integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==, - } - engines: { node: ">=18" } + resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} + engines: {node: '>=18'} lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -4463,65 +3933,38 @@ packages: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} math-intrinsics@1.1.0: - resolution: - { - integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==, - } - engines: { node: ">= 0.4" } + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} media-typer@1.1.0: - resolution: - { - integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==, - } - engines: { node: ">= 0.8" } + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} memory-pager@1.5.0: - resolution: - { - integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==, - } + resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==} merge-descriptors@2.0.0: - resolution: - { - integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==, - } - engines: { node: ">=18" } + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} merge-stream@2.0.0: - resolution: - { - integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==, - } + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} merge2@1.4.1: - resolution: - { - integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==, - } - engines: { node: ">= 8" } + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} micromatch@4.0.8: - resolution: - { - integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==, - } - engines: { node: ">=8.6" } + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} mime-db@1.54.0: - resolution: - { - integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==, - } - engines: { node: ">= 0.6" } + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} mime-types@3.0.2: - resolution: - { - integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==, - } - engines: { node: ">=18" } + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} @@ -4532,27 +3975,18 @@ packages: engines: {node: '>=8'} mimic-function@5.0.1: - resolution: - { - integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==, - } - engines: { node: ">=18" } + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} minimatch@10.2.5: - resolution: - { - integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==, - } - engines: { node: 18 || 20 || >=22 } + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} minimatch@3.1.5: resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} minimist@1.2.8: - resolution: - { - integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==, - } + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} mlly@1.8.2: resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} @@ -4565,23 +3999,20 @@ packages: engines: {node: '>=20.19.0'} mongodb@7.1.0: - resolution: - { - integrity: sha512-kMfnKunbolQYwCIyrkxNJFB4Ypy91pYqua5NargS/f8ODNSJxT03ZU3n1JqL4mCzbSih8tvmMEMLpKTT7x5gCg==, - } - engines: { node: ">=20.19.0" } - peerDependencies: - "@aws-sdk/credential-providers": ^3.806.0 - "@mongodb-js/zstd": ^7.0.0 + resolution: {integrity: sha512-kMfnKunbolQYwCIyrkxNJFB4Ypy91pYqua5NargS/f8ODNSJxT03ZU3n1JqL4mCzbSih8tvmMEMLpKTT7x5gCg==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@aws-sdk/credential-providers': ^3.806.0 + '@mongodb-js/zstd': ^7.0.0 gcp-metadata: ^7.0.1 kerberos: ^7.0.0 - mongodb-client-encryption: ">=7.0.0 <7.1.0" + mongodb-client-encryption: '>=7.0.0 <7.1.0' snappy: ^7.3.2 socks: ^2.8.6 peerDependenciesMeta: - "@aws-sdk/credential-providers": + '@aws-sdk/credential-providers': optional: true - "@mongodb-js/zstd": + '@mongodb-js/zstd': optional: true gcp-metadata: optional: true @@ -4622,11 +4053,8 @@ packages: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} nanoid@3.3.12: - resolution: - { - integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==, - } - engines: { node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1 } + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true nanoid@3.3.18: @@ -4635,40 +4063,31 @@ packages: hasBin: true nanostores@1.2.0: - resolution: - { - integrity: sha512-F0wCzbsH80G7XXo0Jd9/AVQC7ouWY6idUCTnMwW5t/Rv9W8qmO6endavDwg7TNp5GbugwSukFMVZqzPSrSMndg==, - } - engines: { node: ^20.0.0 || >=22.0.0 } + resolution: {integrity: sha512-F0wCzbsH80G7XXo0Jd9/AVQC7ouWY6idUCTnMwW5t/Rv9W8qmO6endavDwg7TNp5GbugwSukFMVZqzPSrSMndg==} + engines: {node: ^20.0.0 || >=22.0.0} natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} negotiator@1.0.0: - resolution: - { - integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==, - } - engines: { node: ">= 0.6" } + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} next@15.5.18: - resolution: - { - integrity: sha512-eKL8zUJkX9Y5lE+RX/2YJoItVdGlIscyVyboeD9wSpp0PaGqjoA4tTpT2qPqz9ax+5IzGESyLSeZ/RCwbSZ2uQ==, - } - engines: { node: ^18.18.0 || ^19.8.0 || >= 20.0.0 } + resolution: {integrity: sha512-eKL8zUJkX9Y5lE+RX/2YJoItVdGlIscyVyboeD9wSpp0PaGqjoA4tTpT2qPqz9ax+5IzGESyLSeZ/RCwbSZ2uQ==} + engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} hasBin: true peerDependencies: - "@opentelemetry/api": ^1.1.0 - "@playwright/test": ^1.51.1 - babel-plugin-react-compiler: "*" + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.51.1 + babel-plugin-react-compiler: '*' react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 sass: ^1.3.0 peerDependenciesMeta: - "@opentelemetry/api": + '@opentelemetry/api': optional: true - "@playwright/test": + '@playwright/test': optional: true babel-plugin-react-compiler: optional: true @@ -4714,38 +4133,23 @@ packages: engines: {node: '>= 0.4'} node-releases@2.0.37: - resolution: - { - integrity: sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==, - } + resolution: {integrity: sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==} npm-run-path@4.0.1: - resolution: - { - integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==, - } - engines: { node: ">=8" } + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} npm-run-path@6.0.0: - resolution: - { - integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==, - } - engines: { node: ">=18" } + resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} + engines: {node: '>=18'} object-assign@4.1.1: - resolution: - { - integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==, - } - engines: { node: ">=0.10.0" } + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} object-inspect@1.13.4: - resolution: - { - integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==, - } - engines: { node: ">= 0.4" } + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} object-keys@1.1.1: resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} @@ -4789,31 +4193,19 @@ packages: resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} on-finished@2.4.1: - resolution: - { - integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==, - } - engines: { node: ">= 0.8" } + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} once@1.4.0: - resolution: - { - integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==, - } + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} onetime@5.1.2: - resolution: - { - integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==, - } - engines: { node: ">=6" } + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} onetime@7.0.0: - resolution: - { - integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==, - } - engines: { node: ">=18" } + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} open@11.0.0: resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==} @@ -4828,11 +4220,8 @@ packages: engines: {node: '>= 0.8.0'} ora@8.2.0: - resolution: - { - integrity: sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==, - } - engines: { node: ">=18" } + resolution: {integrity: sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==} + engines: {node: '>=18'} own-keys@1.0.2: resolution: {integrity: sha512-19YVAg7T+WTrxggPukVq7DjTv6+PJ867TmhCvBsYwmbFCsZd344rq2Ld1p0wo8f8Qrrhgp82c6FJRqdXWtSEhg==} @@ -4897,32 +4286,20 @@ packages: engines: {node: '>=6'} parent-module@1.0.1: - resolution: - { - integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==, - } - engines: { node: ">=6" } + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} parse-json@5.2.0: - resolution: - { - integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==, - } - engines: { node: ">=8" } + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} parse-ms@4.0.0: - resolution: - { - integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==, - } - engines: { node: ">=18" } + resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} + engines: {node: '>=18'} parseurl@1.3.3: - resolution: - { - integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==, - } - engines: { node: ">= 0.8" } + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} @@ -4936,18 +4313,12 @@ packages: engines: {node: '>=8'} path-key@3.1.1: - resolution: - { - integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==, - } - engines: { node: ">=8" } + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} path-key@4.0.0: - resolution: - { - integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==, - } - engines: { node: ">=12" } + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} @@ -4959,28 +4330,19 @@ packages: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} picocolors@1.1.1: - resolution: - { - integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==, - } + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} picomatch@2.3.2: - resolution: - { - integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==, - } - engines: { node: ">=8.6" } + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} picomatch@4.0.5: resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} engines: {node: '>=12'} pkce-challenge@5.0.1: - resolution: - { - integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==, - } - engines: { node: ">=16.20.0" } + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} @@ -5014,11 +4376,8 @@ packages: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} postcss@8.4.31: - resolution: - { - integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==, - } - engines: { node: ^10 || ^12 || >=14 } + resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} + engines: {node: ^10 || ^12 || >=14} postcss@8.5.16: resolution: {integrity: sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==} @@ -5046,45 +4405,27 @@ packages: engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} pretty-ms@9.3.0: - resolution: - { - integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==, - } - engines: { node: ">=18" } + resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} + engines: {node: '>=18'} prompts@2.4.2: - resolution: - { - integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==, - } - engines: { node: ">= 6" } + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} proxy-addr@2.0.7: - resolution: - { - integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==, - } - engines: { node: ">= 0.10" } + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} punycode@2.3.1: - resolution: - { - integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==, - } - engines: { node: ">=6" } + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} pvtsutils@1.3.6: - resolution: - { - integrity: sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==, - } + resolution: {integrity: sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==} pvutils@1.1.5: - resolution: - { - integrity: sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==, - } - engines: { node: ">=16.0.0" } + resolution: {integrity: sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==} + engines: {node: '>=16.0.0'} qs@6.15.3: resolution: {integrity: sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==} @@ -5094,21 +4435,15 @@ packages: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} queue-microtask@1.2.3: - resolution: - { - integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==, - } + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} range-parser@1.3.0: resolution: {integrity: sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==} engines: {node: '>= 0.6'} raw-body@3.0.2: - resolution: - { - integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==, - } - engines: { node: ">= 0.10" } + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} react-day-picker@10.0.1: resolution: {integrity: sha512-eNh6BlwcYInWaJtRv18mXQ06Ys/H6rdTZAnTaSdOYJuTpwP1JMCHNd1FDRadA+gbeinq+psdULN5Xnowy9mV8w==} @@ -5129,39 +4464,33 @@ packages: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} react-remove-scroll-bar@2.3.8: - resolution: - { - integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==, - } - engines: { node: ">=10" } + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} peerDependencies: - "@types/react": "*" + '@types/react': '*' react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 peerDependenciesMeta: - "@types/react": + '@types/react': optional: true react-remove-scroll@2.7.2: resolution: {integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==} engines: {node: '>=10'} peerDependencies: - "@types/react": "*" + '@types/react': '*' react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true react-style-singleton@2.2.3: - resolution: - { - integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==, - } - engines: { node: ">=10" } + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + engines: {node: '>=10'} peerDependencies: - "@types/react": "*" + '@types/react': '*' react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true react@19.2.7: @@ -5181,10 +4510,7 @@ packages: engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} reflect-metadata@0.2.2: - resolution: - { - integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==, - } + resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} reflect.getprototypeof@1.0.10: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} @@ -5199,11 +4525,8 @@ packages: engines: {node: '>= 0.4'} require-from-string@2.0.2: - resolution: - { - integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==, - } - engines: { node: ">=0.10.0" } + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} requireindex@1.2.0: resolution: {integrity: sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==} @@ -5213,11 +4536,8 @@ packages: resolution: {integrity: sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==} resolve-from@4.0.0: - resolution: - { - integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==, - } - engines: { node: ">=4" } + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} resolve@2.0.0-next.7: resolution: {integrity: sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==} @@ -5225,11 +4545,8 @@ packages: hasBin: true restore-cursor@5.1.0: - resolution: - { - integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==, - } - engines: { node: ">=18" } + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} @@ -5247,24 +4564,15 @@ packages: resolution: {integrity: sha512-ePa+XGk00/3HuCqrEnK3LxJW7I0SdNg6EFzKUJG73hMAdDcOUC/i/aSz7LSDwLrGr33kal/rqOGydzwl6U7zBA==} router@2.2.0: - resolution: - { - integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==, - } - engines: { node: ">= 18" } + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} run-applescript@7.1.0: - resolution: - { - integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==, - } - engines: { node: ">=18" } + resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} + engines: {node: '>=18'} run-parallel@1.2.0: - resolution: - { - integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==, - } + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} safe-array-concat@1.1.4: resolution: {integrity: sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==} @@ -5279,10 +4587,7 @@ packages: engines: {node: '>= 0.4'} safer-buffer@2.1.2: - resolution: - { - integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==, - } + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -5292,10 +4597,7 @@ packages: engines: {node: ^14.0.0 || >=16.0.0} semver@6.3.1: - resolution: - { - integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==, - } + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true semver@7.8.2: @@ -5333,17 +4635,11 @@ packages: engines: {node: '>=10'} serve-static@2.2.1: - resolution: - { - integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==, - } - engines: { node: ">= 18" } + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} set-cookie-parser@3.0.1: - resolution: - { - integrity: sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q==, - } + resolution: {integrity: sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q==} set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} @@ -5358,10 +4654,7 @@ packages: engines: {node: '>= 0.4'} setprototypeof@1.2.0: - resolution: - { - integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==, - } + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} shadcn@4.13.0: resolution: {integrity: sha512-5fuJ4jI/GcPeA/iTL4cJivCZuYQGXz/N3bIzyd+Gd/FM6xUCy2MxGG+LaDQuw2cjNy9zGPSFPTEmI048UwPTZA==} @@ -5369,46 +4662,28 @@ packages: hasBin: true sharp@0.34.5: - resolution: - { - integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==, - } - engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} shebang-command@2.0.0: - resolution: - { - integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==, - } - engines: { node: ">=8" } + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} shebang-regex@3.0.0: - resolution: - { - integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==, - } - engines: { node: ">=8" } + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} side-channel-list@1.0.1: - resolution: - { - integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==, - } - engines: { node: ">= 0.4" } + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} side-channel-map@1.0.1: - resolution: - { - integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==, - } - engines: { node: ">= 0.4" } + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} side-channel-weakmap@1.0.2: - resolution: - { - integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==, - } - engines: { node: ">= 0.4" } + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} side-channel@1.1.1: resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} @@ -5418,27 +4693,18 @@ packages: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} signal-exit@3.0.7: - resolution: - { - integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==, - } + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} signal-exit@4.1.0: - resolution: - { - integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==, - } - engines: { node: ">=14" } + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} sirv@3.0.2: resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} engines: {node: '>=18'} sisteransi@1.0.5: - resolution: - { - integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==, - } + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} solid-js@1.9.14: resolution: {integrity: sha512-sAEXC0Kk0S1EDg+8ysEWJDbYhA3RRoEjwuySUGlKIemeo0I5YZfOyumNjNs9Sv3y2nmhD+0rW66ag2HsMuQiGQ==} @@ -5450,11 +4716,8 @@ packages: react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc source-map-js@1.2.1: - resolution: - { - integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==, - } - engines: { node: ">=0.10.0" } + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} source-map@0.6.1: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} @@ -5481,32 +4744,23 @@ packages: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} statuses@2.0.2: - resolution: - { - integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==, - } - engines: { node: ">= 0.8" } + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} std-env@4.2.0: resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} stdin-discarder@0.2.2: - resolution: - { - integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==, - } - engines: { node: ">=18" } + resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} + engines: {node: '>=18'} stop-iteration-iterator@1.1.0: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} string-width@7.2.0: - resolution: - { - integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==, - } - engines: { node: ">=18" } + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} string.prototype.includes@2.0.1: resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==} @@ -5525,46 +4779,28 @@ packages: engines: {node: '>= 0.4'} stringify-object@5.0.0: - resolution: - { - integrity: sha512-zaJYxz2FtcMb4f+g60KsRNFOpVMUyuJgA51Zi5Z1DOTC3S59+OQiVOzE9GZt0x72uBGWKsQIuBKeF9iusmKFsg==, - } - engines: { node: ">=14.16" } + resolution: {integrity: sha512-zaJYxz2FtcMb4f+g60KsRNFOpVMUyuJgA51Zi5Z1DOTC3S59+OQiVOzE9GZt0x72uBGWKsQIuBKeF9iusmKFsg==} + engines: {node: '>=14.16'} strip-ansi@6.0.1: - resolution: - { - integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==, - } - engines: { node: ">=8" } + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} strip-ansi@7.2.0: - resolution: - { - integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==, - } - engines: { node: ">=12" } + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} strip-bom@3.0.0: - resolution: - { - integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==, - } - engines: { node: ">=4" } + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} strip-final-newline@2.0.0: - resolution: - { - integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==, - } - engines: { node: ">=6" } + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} strip-final-newline@4.0.0: - resolution: - { - integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==, - } - engines: { node: ">=18" } + resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} + engines: {node: '>=18'} strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} @@ -5574,27 +4810,21 @@ packages: resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} styled-jsx@5.1.6: - resolution: - { - integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==, - } - engines: { node: ">= 12.0.0" } - peerDependencies: - "@babel/core": "*" - babel-plugin-macros: "*" - react: ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' peerDependenciesMeta: - "@babel/core": + '@babel/core': optional: true babel-plugin-macros: optional: true superjson@2.2.2: - resolution: - { - integrity: sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==, - } - engines: { node: ">=16" } + resolution: {integrity: sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==} + engines: {node: '>=16'} supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} @@ -5629,10 +4859,7 @@ packages: engines: {node: '>=6'} tiny-invariant@1.3.3: - resolution: - { - integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==, - } + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -5654,29 +4881,20 @@ packages: engines: {node: '>=14.0.0'} to-regex-range@5.0.1: - resolution: - { - integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==, - } - engines: { node: ">=8.0" } + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} toidentifier@1.0.1: - resolution: - { - integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==, - } - engines: { node: ">=0.6" } + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} totalist@3.0.1: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} tr46@5.1.1: - resolution: - { - integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==, - } - engines: { node: ">=18" } + resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} + engines: {node: '>=18'} ts-api-utils@2.5.0: resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} @@ -5685,45 +4903,27 @@ packages: typescript: '>=4.8.4' ts-morph@26.0.0: - resolution: - { - integrity: sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug==, - } + resolution: {integrity: sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug==} tsconfig-paths@3.15.0: resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} tsconfig-paths@4.2.0: - resolution: - { - integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==, - } - engines: { node: ">=6" } + resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} + engines: {node: '>=6'} tslib@1.14.1: - resolution: - { - integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==, - } + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} tslib@2.8.1: - resolution: - { - integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==, - } + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} tsyringe@4.10.0: - resolution: - { - integrity: sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==, - } - engines: { node: ">= 6.0.0" } + resolution: {integrity: sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==} + engines: {node: '>= 6.0.0'} tw-animate-css@1.4.0: - resolution: - { - integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==, - } + resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==} type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} @@ -5779,25 +4979,16 @@ packages: resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} unicorn-magic@0.3.0: - resolution: - { - integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==, - } - engines: { node: ">=18" } + resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} + engines: {node: '>=18'} universalify@2.0.1: - resolution: - { - integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==, - } - engines: { node: ">= 10.0.0" } + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} unpipe@1.0.0: - resolution: - { - integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==, - } - engines: { node: ">= 0.8" } + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} unplugin@3.3.0: resolution: {integrity: sha512-qa66K+crbfyE6JK10GjvbJeRrOsuC/JpbnHctfyp/i4oBTxWOzJfRZyDiOk1PtErMFRu8JhsU/wPvOdBNWe5Rg==} @@ -5907,70 +5098,49 @@ packages: optional: true update-browserslist-db@1.2.3: - resolution: - { - integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==, - } + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true peerDependencies: - browserslist: ">= 4.21.0" + browserslist: '>= 4.21.0' uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} use-callback-ref@1.3.3: - resolution: - { - integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==, - } - engines: { node: ">=10" } + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + engines: {node: '>=10'} peerDependencies: - "@types/react": "*" + '@types/react': '*' react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true use-sidecar@1.1.3: - resolution: - { - integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==, - } - engines: { node: ">=10" } + resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} + engines: {node: '>=10'} peerDependencies: - "@types/react": "*" + '@types/react': '*' react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true use-sync-external-store@1.6.0: - resolution: - { - integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==, - } + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 util-deprecate@1.0.2: - resolution: - { - integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==, - } + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} validate-npm-package-name@7.0.2: - resolution: - { - integrity: sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==, - } - engines: { node: ^20.17.0 || >=22.9.0 } + resolution: {integrity: sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==} + engines: {node: ^20.17.0 || >=22.9.0} vary@1.1.2: - resolution: - { - integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==, - } - engines: { node: ">= 0.8" } + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} vite-plus@0.2.9: resolution: {integrity: sha512-8uRNqAxh9no3AU4Lep8BEYhkim07+3NO+mhuxTWiN0k30syGT/2+ue/DtWYhtzQ7yi2f2WKpjOhoI4/QkWWbUg==} @@ -6042,11 +5212,8 @@ packages: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} whatwg-url@14.2.0: - resolution: - { - integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==, - } - engines: { node: ">=18" } + resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} + engines: {node: '>=18'} which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} @@ -6065,19 +5232,13 @@ packages: engines: {node: '>= 0.4'} which@2.0.2: - resolution: - { - integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==, - } - engines: { node: ">= 8" } + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} hasBin: true which@4.0.0: - resolution: - { - integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==, - } - engines: { node: ^16.13.0 || >=18.0.0 } + resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==} + engines: {node: ^16.13.0 || >=18.0.0} hasBin: true why-is-node-running@2.3.0: @@ -6090,10 +5251,7 @@ packages: engines: {node: '>=0.10.0'} wrappy@1.0.2: - resolution: - { - integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==, - } + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} ws@8.21.3: resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} @@ -6108,21 +5266,15 @@ packages: optional: true wsl-utils@0.3.1: - resolution: - { - integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==, - } - engines: { node: ">=20" } + resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==} + engines: {node: '>=20'} xmlbuilder2@4.0.3: resolution: {integrity: sha512-bx8Q1STctnNaaDymWnkfQLKofs0mGNN7rLLapJlGuV3VlvegD7Ls4ggMjE3aUSWItCCzU0PEv45lI87iSigiCA==} engines: {node: '>=20.0'} yallist@3.1.1: - resolution: - { - integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==, - } + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} yaml@2.9.0: resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} @@ -6138,11 +5290,8 @@ packages: engines: {node: '>=18.19'} yoctocolors@2.1.2: - resolution: - { - integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==, - } - engines: { node: ">=18" } + resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} + engines: {node: '>=18'} yuku-codegen@0.5.48: resolution: {integrity: sha512-p7HxD5Xl4jzDzqMrGePAOeSHmRY4g58h4HuGq15weQFPxuPWd/W6e7nqp/+Lea6JfpOdBwJOAyXFqIZ/J9Zfnw==} @@ -6151,24 +5300,15 @@ packages: resolution: {integrity: sha512-OWBfhrpgK9+/4+IXG9oT8Bao4AhViQA7vdyNNH7EUg8dQYgwa70XtIBWTpCEme1P1ECyoDNYkn0wT63f8XRcVA==} zod-to-json-schema@3.25.2: - resolution: - { - integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==, - } + resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} peerDependencies: zod: ^3.25.28 || ^4 zod@3.25.76: - resolution: - { - integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==, - } + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} zod@4.3.5: - resolution: - { - integrity: sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==, - } + resolution: {integrity: sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==} zod@4.3.6: resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} @@ -6177,7 +5317,6 @@ packages: resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} snapshots: - "@alloc/quick-lru@5.2.0": {} '@babel/code-frame@7.27.1': dependencies: @@ -6187,7 +5326,7 @@ snapshots: '@babel/code-frame@7.29.0': dependencies: - "@babel/helper-validator-identifier": 7.28.5 + '@babel/helper-validator-identifier': 7.28.5 js-tokens: 4.0.0 picocolors: 1.1.1 @@ -6199,18 +5338,18 @@ snapshots: '@babel/compat-data@7.29.0': {} - "@babel/core@7.29.0": - dependencies: - "@babel/code-frame": 7.29.0 - "@babel/generator": 7.29.1 - "@babel/helper-compilation-targets": 7.28.6 - "@babel/helper-module-transforms": 7.28.6(@babel/core@7.29.0) - "@babel/helpers": 7.29.2 - "@babel/parser": 7.29.2 - "@babel/template": 7.28.6 - "@babel/traverse": 7.29.0 - "@babel/types": 7.29.0 - "@jridgewell/remapping": 2.3.5 + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.29.2 + '@babel/parser': 7.29.2 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 debug: 4.4.3 gensync: 1.0.0-beta.2 @@ -6219,12 +5358,12 @@ snapshots: transitivePeerDependencies: - supports-color - "@babel/generator@7.29.1": + '@babel/generator@7.29.1': dependencies: - "@babel/parser": 7.29.2 - "@babel/types": 7.29.0 - "@jridgewell/gen-mapping": 0.3.13 - "@jridgewell/trace-mapping": 0.3.31 + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 '@babel/generator@7.29.7': @@ -6239,10 +5378,10 @@ snapshots: dependencies: '@babel/types': 7.29.7 - "@babel/helper-compilation-targets@7.28.6": + '@babel/helper-compilation-targets@7.28.6': dependencies: - "@babel/compat-data": 7.29.0 - "@babel/helper-validator-option": 7.27.1 + '@babel/compat-data': 7.29.0 + '@babel/helper-validator-option': 7.27.1 browserslist: 4.28.2 lru-cache: 5.1.1 semver: 6.3.1 @@ -6260,7 +5399,7 @@ snapshots: transitivePeerDependencies: - supports-color - "@babel/helper-globals@7.28.0": {} + '@babel/helper-globals@7.28.0': {} '@babel/helper-globals@7.29.7': {} @@ -6271,10 +5410,10 @@ snapshots: transitivePeerDependencies: - supports-color - "@babel/helper-module-imports@7.28.6": + '@babel/helper-module-imports@7.28.6': dependencies: - "@babel/traverse": 7.29.0 - "@babel/types": 7.29.0 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 transitivePeerDependencies: - supports-color @@ -6287,10 +5426,10 @@ snapshots: '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': dependencies: - "@babel/core": 7.29.0 - "@babel/helper-module-imports": 7.28.6 - "@babel/helper-validator-identifier": 7.28.5 - "@babel/traverse": 7.29.0 + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 transitivePeerDependencies: - supports-color @@ -6325,7 +5464,7 @@ snapshots: transitivePeerDependencies: - supports-color - "@babel/helper-string-parser@7.27.1": {} + '@babel/helper-string-parser@7.27.1': {} '@babel/helper-string-parser@7.29.7': {} @@ -6337,14 +5476,14 @@ snapshots: '@babel/helper-validator-option@7.29.7': {} - "@babel/helpers@7.29.2": + '@babel/helpers@7.29.2': dependencies: - "@babel/template": 7.28.6 - "@babel/types": 7.29.0 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 - "@babel/parser@7.29.2": + '@babel/parser@7.29.2': dependencies: - "@babel/types": 7.29.0 + '@babel/types': 7.29.0 '@babel/parser@7.29.7': dependencies: @@ -6392,11 +5531,11 @@ snapshots: '@babel/runtime@7.29.7': {} - "@babel/template@7.28.6": + '@babel/template@7.28.6': dependencies: - "@babel/code-frame": 7.29.0 - "@babel/parser": 7.29.2 - "@babel/types": 7.29.0 + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 '@babel/template@7.29.7': dependencies: @@ -6406,12 +5545,12 @@ snapshots: '@babel/traverse@7.29.0': dependencies: - "@babel/code-frame": 7.29.0 - "@babel/generator": 7.29.1 - "@babel/helper-globals": 7.28.0 - "@babel/parser": 7.29.2 - "@babel/template": 7.28.6 - "@babel/types": 7.29.0 + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.2 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -6428,10 +5567,10 @@ snapshots: transitivePeerDependencies: - supports-color - "@babel/types@7.29.0": + '@babel/types@7.29.0': dependencies: - "@babel/helper-string-parser": 7.27.1 - "@babel/helper-validator-identifier": 7.28.5 + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 '@babel/types@7.29.7': dependencies: @@ -6469,37 +5608,37 @@ snapshots: optionalDependencies: '@types/react': 19.2.17 - "@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.2.0)": + '@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.2.0)': dependencies: - "@better-auth/utils": 0.3.1 - "@better-fetch/fetch": 1.1.21 - "@standard-schema/spec": 1.1.0 + '@better-auth/utils': 0.3.1 + '@better-fetch/fetch': 1.1.21 + '@standard-schema/spec': 1.1.0 better-call: 1.3.2(zod@4.3.5) jose: 6.1.3 kysely: 0.28.12 nanostores: 1.2.0 zod: 4.3.6 - "@better-auth/drizzle-adapter@1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.2.0))(@better-auth/utils@0.3.1)": + '@better-auth/drizzle-adapter@1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.2.0))(@better-auth/utils@0.3.1)': dependencies: - "@better-auth/core": 1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.2.0) - "@better-auth/utils": 0.3.1 + '@better-auth/core': 1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.2.0) + '@better-auth/utils': 0.3.1 - "@better-auth/kysely-adapter@1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(kysely@0.28.12)": + '@better-auth/kysely-adapter@1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(kysely@0.28.12)': dependencies: - "@better-auth/core": 1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.2.0) - "@better-auth/utils": 0.3.1 + '@better-auth/core': 1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.2.0) + '@better-auth/utils': 0.3.1 kysely: 0.28.12 - "@better-auth/memory-adapter@1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.2.0))(@better-auth/utils@0.3.1)": + '@better-auth/memory-adapter@1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.2.0))(@better-auth/utils@0.3.1)': dependencies: - "@better-auth/core": 1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.2.0) - "@better-auth/utils": 0.3.1 + '@better-auth/core': 1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.2.0) + '@better-auth/utils': 0.3.1 - "@better-auth/mongo-adapter@1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(mongodb@7.1.0)": + '@better-auth/mongo-adapter@1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(mongodb@7.1.0)': dependencies: - "@better-auth/core": 1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.2.0) - "@better-auth/utils": 0.3.1 + '@better-auth/core': 1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.2.0) + '@better-auth/utils': 0.3.1 mongodb: 7.1.0 '@better-auth/passkey@1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-auth@1.5.5(@tanstack/react-start@1.168.27(@voidzero-dev/vite-plus-core@0.2.9(@types/node@22.20.1)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0))(crossws@0.4.9(srvx@0.11.21))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(rolldown@1.1.5))(mongodb@7.1.0)(next@15.5.18(@babel/core@7.29.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(solid-js@1.9.14)(vitest@4.1.10))(better-call@1.3.2(zod@4.3.5))(nanostores@1.2.0)': @@ -6514,20 +5653,20 @@ snapshots: nanostores: 1.2.0 zod: 4.3.6 - "@better-auth/prisma-adapter@1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.2.0))(@better-auth/utils@0.3.1)": + '@better-auth/prisma-adapter@1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.2.0))(@better-auth/utils@0.3.1)': dependencies: - "@better-auth/core": 1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.2.0) - "@better-auth/utils": 0.3.1 + '@better-auth/core': 1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.2.0) + '@better-auth/utils': 0.3.1 - "@better-auth/telemetry@1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.2.0))": + '@better-auth/telemetry@1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.2.0))': dependencies: - "@better-auth/core": 1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.2.0) - "@better-auth/utils": 0.3.1 - "@better-fetch/fetch": 1.1.21 + '@better-auth/core': 1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.2.0) + '@better-auth/utils': 0.3.1 + '@better-fetch/fetch': 1.1.21 - "@better-auth/utils@0.3.1": {} + '@better-auth/utils@0.3.1': {} - "@better-fetch/fetch@1.1.21": {} + '@better-fetch/fetch@1.1.21': {} '@blazediff/core@1.9.1': {} @@ -6679,12 +5818,12 @@ snapshots: '@floating-ui/core@1.7.5': dependencies: - "@floating-ui/utils": 0.2.11 + '@floating-ui/utils': 0.2.11 '@floating-ui/dom@1.7.6': dependencies: - "@floating-ui/core": 1.7.5 - "@floating-ui/utils": 0.2.11 + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 '@floating-ui/react-dom@2.1.8(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: @@ -6721,122 +5860,122 @@ snapshots: '@img/colour@1.1.0': optional: true - "@img/sharp-darwin-arm64@0.34.5": + '@img/sharp-darwin-arm64@0.34.5': optionalDependencies: - "@img/sharp-libvips-darwin-arm64": 1.2.4 + '@img/sharp-libvips-darwin-arm64': 1.2.4 optional: true - "@img/sharp-darwin-x64@0.34.5": + '@img/sharp-darwin-x64@0.34.5': optionalDependencies: - "@img/sharp-libvips-darwin-x64": 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 optional: true - "@img/sharp-libvips-darwin-arm64@1.2.4": + '@img/sharp-libvips-darwin-arm64@1.2.4': optional: true - "@img/sharp-libvips-darwin-x64@1.2.4": + '@img/sharp-libvips-darwin-x64@1.2.4': optional: true - "@img/sharp-libvips-linux-arm64@1.2.4": + '@img/sharp-libvips-linux-arm64@1.2.4': optional: true - "@img/sharp-libvips-linux-arm@1.2.4": + '@img/sharp-libvips-linux-arm@1.2.4': optional: true - "@img/sharp-libvips-linux-ppc64@1.2.4": + '@img/sharp-libvips-linux-ppc64@1.2.4': optional: true - "@img/sharp-libvips-linux-riscv64@1.2.4": + '@img/sharp-libvips-linux-riscv64@1.2.4': optional: true - "@img/sharp-libvips-linux-s390x@1.2.4": + '@img/sharp-libvips-linux-s390x@1.2.4': optional: true - "@img/sharp-libvips-linux-x64@1.2.4": + '@img/sharp-libvips-linux-x64@1.2.4': optional: true - "@img/sharp-libvips-linuxmusl-arm64@1.2.4": + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': optional: true - "@img/sharp-libvips-linuxmusl-x64@1.2.4": + '@img/sharp-libvips-linuxmusl-x64@1.2.4': optional: true - "@img/sharp-linux-arm64@0.34.5": + '@img/sharp-linux-arm64@0.34.5': optionalDependencies: - "@img/sharp-libvips-linux-arm64": 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 optional: true - "@img/sharp-linux-arm@0.34.5": + '@img/sharp-linux-arm@0.34.5': optionalDependencies: - "@img/sharp-libvips-linux-arm": 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 optional: true - "@img/sharp-linux-ppc64@0.34.5": + '@img/sharp-linux-ppc64@0.34.5': optionalDependencies: - "@img/sharp-libvips-linux-ppc64": 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 optional: true - "@img/sharp-linux-riscv64@0.34.5": + '@img/sharp-linux-riscv64@0.34.5': optionalDependencies: - "@img/sharp-libvips-linux-riscv64": 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 optional: true - "@img/sharp-linux-s390x@0.34.5": + '@img/sharp-linux-s390x@0.34.5': optionalDependencies: - "@img/sharp-libvips-linux-s390x": 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 optional: true - "@img/sharp-linux-x64@0.34.5": + '@img/sharp-linux-x64@0.34.5': optionalDependencies: - "@img/sharp-libvips-linux-x64": 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 optional: true - "@img/sharp-linuxmusl-arm64@0.34.5": + '@img/sharp-linuxmusl-arm64@0.34.5': optionalDependencies: - "@img/sharp-libvips-linuxmusl-arm64": 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 optional: true - "@img/sharp-linuxmusl-x64@0.34.5": + '@img/sharp-linuxmusl-x64@0.34.5': optionalDependencies: - "@img/sharp-libvips-linuxmusl-x64": 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 optional: true - "@img/sharp-wasm32@0.34.5": + '@img/sharp-wasm32@0.34.5': dependencies: '@emnapi/runtime': 1.11.3 optional: true - "@img/sharp-win32-arm64@0.34.5": + '@img/sharp-win32-arm64@0.34.5': optional: true - "@img/sharp-win32-ia32@0.34.5": + '@img/sharp-win32-ia32@0.34.5': optional: true - "@img/sharp-win32-x64@0.34.5": + '@img/sharp-win32-x64@0.34.5': optional: true '@jridgewell/gen-mapping@0.3.13': dependencies: - "@jridgewell/sourcemap-codec": 1.5.5 - "@jridgewell/trace-mapping": 0.3.31 + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 - "@jridgewell/remapping@2.3.5": + '@jridgewell/remapping@2.3.5': dependencies: - "@jridgewell/gen-mapping": 0.3.13 - "@jridgewell/trace-mapping": 0.3.31 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 - "@jridgewell/resolve-uri@3.1.2": {} + '@jridgewell/resolve-uri@3.1.2': {} - "@jridgewell/sourcemap-codec@1.5.5": {} + '@jridgewell/sourcemap-codec@1.5.5': {} - "@jridgewell/trace-mapping@0.3.31": + '@jridgewell/trace-mapping@0.3.31': dependencies: - "@jridgewell/resolve-uri": 3.1.2 - "@jridgewell/sourcemap-codec": 1.5.5 + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 - "@levischuck/tiny-cbor@0.2.11": {} + '@levischuck/tiny-cbor@0.2.11': {} - "@modelcontextprotocol/sdk@1.29.0(zod@3.25.76)": + '@modelcontextprotocol/sdk@1.29.0(zod@3.25.76)': dependencies: '@hono/node-server': 1.19.14(hono@4.12.29) ajv: 8.20.0 @@ -7126,97 +6265,97 @@ snapshots: '@oxlint/plugins@1.79.0': {} - "@peculiar/asn1-android@2.6.0": + '@peculiar/asn1-android@2.6.0': dependencies: - "@peculiar/asn1-schema": 2.6.0 + '@peculiar/asn1-schema': 2.6.0 asn1js: 3.0.7 tslib: 2.8.1 - "@peculiar/asn1-cms@2.6.0": + '@peculiar/asn1-cms@2.6.0': dependencies: - "@peculiar/asn1-schema": 2.6.0 - "@peculiar/asn1-x509": 2.6.1 - "@peculiar/asn1-x509-attr": 2.6.0 + '@peculiar/asn1-schema': 2.6.0 + '@peculiar/asn1-x509': 2.6.1 + '@peculiar/asn1-x509-attr': 2.6.0 asn1js: 3.0.7 tslib: 2.8.1 - "@peculiar/asn1-csr@2.6.0": + '@peculiar/asn1-csr@2.6.0': dependencies: - "@peculiar/asn1-schema": 2.6.0 - "@peculiar/asn1-x509": 2.6.1 + '@peculiar/asn1-schema': 2.6.0 + '@peculiar/asn1-x509': 2.6.1 asn1js: 3.0.7 tslib: 2.8.1 - "@peculiar/asn1-ecc@2.6.1": + '@peculiar/asn1-ecc@2.6.1': dependencies: - "@peculiar/asn1-schema": 2.6.0 - "@peculiar/asn1-x509": 2.6.1 + '@peculiar/asn1-schema': 2.6.0 + '@peculiar/asn1-x509': 2.6.1 asn1js: 3.0.7 tslib: 2.8.1 - "@peculiar/asn1-pfx@2.6.0": + '@peculiar/asn1-pfx@2.6.0': dependencies: - "@peculiar/asn1-cms": 2.6.0 - "@peculiar/asn1-pkcs8": 2.6.0 - "@peculiar/asn1-rsa": 2.6.1 - "@peculiar/asn1-schema": 2.6.0 + '@peculiar/asn1-cms': 2.6.0 + '@peculiar/asn1-pkcs8': 2.6.0 + '@peculiar/asn1-rsa': 2.6.1 + '@peculiar/asn1-schema': 2.6.0 asn1js: 3.0.7 tslib: 2.8.1 - "@peculiar/asn1-pkcs8@2.6.0": + '@peculiar/asn1-pkcs8@2.6.0': dependencies: - "@peculiar/asn1-schema": 2.6.0 - "@peculiar/asn1-x509": 2.6.1 + '@peculiar/asn1-schema': 2.6.0 + '@peculiar/asn1-x509': 2.6.1 asn1js: 3.0.7 tslib: 2.8.1 - "@peculiar/asn1-pkcs9@2.6.0": + '@peculiar/asn1-pkcs9@2.6.0': dependencies: - "@peculiar/asn1-cms": 2.6.0 - "@peculiar/asn1-pfx": 2.6.0 - "@peculiar/asn1-pkcs8": 2.6.0 - "@peculiar/asn1-schema": 2.6.0 - "@peculiar/asn1-x509": 2.6.1 - "@peculiar/asn1-x509-attr": 2.6.0 + '@peculiar/asn1-cms': 2.6.0 + '@peculiar/asn1-pfx': 2.6.0 + '@peculiar/asn1-pkcs8': 2.6.0 + '@peculiar/asn1-schema': 2.6.0 + '@peculiar/asn1-x509': 2.6.1 + '@peculiar/asn1-x509-attr': 2.6.0 asn1js: 3.0.7 tslib: 2.8.1 - "@peculiar/asn1-rsa@2.6.1": + '@peculiar/asn1-rsa@2.6.1': dependencies: - "@peculiar/asn1-schema": 2.6.0 - "@peculiar/asn1-x509": 2.6.1 + '@peculiar/asn1-schema': 2.6.0 + '@peculiar/asn1-x509': 2.6.1 asn1js: 3.0.7 tslib: 2.8.1 - "@peculiar/asn1-schema@2.6.0": + '@peculiar/asn1-schema@2.6.0': dependencies: asn1js: 3.0.7 pvtsutils: 1.3.6 tslib: 2.8.1 - "@peculiar/asn1-x509-attr@2.6.0": + '@peculiar/asn1-x509-attr@2.6.0': dependencies: - "@peculiar/asn1-schema": 2.6.0 - "@peculiar/asn1-x509": 2.6.1 + '@peculiar/asn1-schema': 2.6.0 + '@peculiar/asn1-x509': 2.6.1 asn1js: 3.0.7 tslib: 2.8.1 - "@peculiar/asn1-x509@2.6.1": + '@peculiar/asn1-x509@2.6.1': dependencies: - "@peculiar/asn1-schema": 2.6.0 + '@peculiar/asn1-schema': 2.6.0 asn1js: 3.0.7 pvtsutils: 1.3.6 tslib: 2.8.1 - "@peculiar/x509@1.14.3": + '@peculiar/x509@1.14.3': dependencies: - "@peculiar/asn1-cms": 2.6.0 - "@peculiar/asn1-csr": 2.6.0 - "@peculiar/asn1-ecc": 2.6.1 - "@peculiar/asn1-pkcs9": 2.6.0 - "@peculiar/asn1-rsa": 2.6.1 - "@peculiar/asn1-schema": 2.6.0 - "@peculiar/asn1-x509": 2.6.1 + '@peculiar/asn1-cms': 2.6.0 + '@peculiar/asn1-csr': 2.6.0 + '@peculiar/asn1-ecc': 2.6.1 + '@peculiar/asn1-pkcs9': 2.6.0 + '@peculiar/asn1-rsa': 2.6.1 + '@peculiar/asn1-schema': 2.6.0 + '@peculiar/asn1-x509': 2.6.1 pvtsutils: 1.3.6 reflect-metadata: 0.2.2 tslib: 2.8.1 @@ -7224,7 +6363,7 @@ snapshots: '@pkgr/core@0.3.6': {} - '@polinetwork/backend@0.17.1': {} + '@polinetwork/backend@0.18.0': {} '@polka/url@1.0.0-next.29': {} @@ -8321,17 +7460,17 @@ snapshots: better-auth@1.5.5(@tanstack/react-start@1.168.27(@voidzero-dev/vite-plus-core@0.2.9(@types/node@22.20.1)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0))(crossws@0.4.9(srvx@0.11.21))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(rolldown@1.1.5))(mongodb@7.1.0)(next@15.5.18(@babel/core@7.29.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(solid-js@1.9.14)(vitest@4.1.10): dependencies: - "@better-auth/core": 1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.2.0) - "@better-auth/drizzle-adapter": 1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.2.0))(@better-auth/utils@0.3.1) - "@better-auth/kysely-adapter": 1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(kysely@0.28.12) - "@better-auth/memory-adapter": 1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.2.0))(@better-auth/utils@0.3.1) - "@better-auth/mongo-adapter": 1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(mongodb@7.1.0) - "@better-auth/prisma-adapter": 1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.2.0))(@better-auth/utils@0.3.1) - "@better-auth/telemetry": 1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.2.0)) - "@better-auth/utils": 0.3.1 - "@better-fetch/fetch": 1.1.21 - "@noble/ciphers": 2.1.1 - "@noble/hashes": 2.0.1 + '@better-auth/core': 1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.2.0) + '@better-auth/drizzle-adapter': 1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.2.0))(@better-auth/utils@0.3.1) + '@better-auth/kysely-adapter': 1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(kysely@0.28.12) + '@better-auth/memory-adapter': 1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.2.0))(@better-auth/utils@0.3.1) + '@better-auth/mongo-adapter': 1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(mongodb@7.1.0) + '@better-auth/prisma-adapter': 1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.2.0))(@better-auth/utils@0.3.1) + '@better-auth/telemetry': 1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.2.0)) + '@better-auth/utils': 0.3.1 + '@better-fetch/fetch': 1.1.21 + '@noble/ciphers': 2.1.1 + '@noble/hashes': 2.0.1 better-call: 1.3.2(zod@4.3.5) defu: 6.1.4 jose: 6.1.3 @@ -8347,12 +7486,12 @@ snapshots: solid-js: 1.9.14 vitest: 4.1.10(@types/node@22.20.1)(@vitest/browser-preview@4.1.10)(@voidzero-dev/vite-plus-core@0.2.9(@types/node@22.20.1)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0)) transitivePeerDependencies: - - "@cloudflare/workers-types" + - '@cloudflare/workers-types' better-call@1.3.2(zod@4.3.5): dependencies: - "@better-auth/utils": 0.3.1 - "@better-fetch/fetch": 1.1.21 + '@better-auth/utils': 0.3.1 + '@better-fetch/fetch': 1.1.21 rou3: 0.7.12 set-cookie-parser: 3.0.1 optionalDependencies: @@ -8459,8 +7598,8 @@ snapshots: react: 19.2.7 react-dom: 19.2.7(react@19.2.7) transitivePeerDependencies: - - "@types/react" - - "@types/react-dom" + - '@types/react' + - '@types/react-dom' code-block-writer@13.0.3: {} @@ -9101,7 +8240,7 @@ snapshots: execa@9.6.1: dependencies: - "@sindresorhus/merge-streams": 4.0.0 + '@sindresorhus/merge-streams': 4.0.0 cross-spawn: 7.0.6 figures: 6.1.0 get-stream: 9.0.1 @@ -9162,8 +8301,8 @@ snapshots: fast-glob@3.3.3: dependencies: - "@nodelib/fs.stat": 2.0.5 - "@nodelib/fs.walk": 1.2.8 + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 glob-parent: 5.1.2 merge2: 1.4.1 micromatch: 4.0.8 @@ -9301,7 +8440,7 @@ snapshots: get-stream@9.0.1: dependencies: - "@sec-ant/readable-stream": 0.4.1 + '@sec-ant/readable-stream': 0.4.1 is-stream: 4.0.1 get-symbol-description@1.1.0: @@ -9854,7 +8993,7 @@ snapshots: mongodb-connection-string-url@7.0.2: dependencies: - "@types/whatwg-url": 13.0.0 + '@types/whatwg-url': 13.0.0 whatwg-url: 14.2.0 mongodb@7.1.0: @@ -9902,18 +9041,18 @@ snapshots: react-dom: 19.2.7(react@19.2.7) styled-jsx: 5.1.6(@babel/core@7.29.0)(react@19.2.7) optionalDependencies: - "@next/swc-darwin-arm64": 15.5.18 - "@next/swc-darwin-x64": 15.5.18 - "@next/swc-linux-arm64-gnu": 15.5.18 - "@next/swc-linux-arm64-musl": 15.5.18 - "@next/swc-linux-x64-gnu": 15.5.18 - "@next/swc-linux-x64-musl": 15.5.18 - "@next/swc-win32-arm64-msvc": 15.5.18 - "@next/swc-win32-x64-msvc": 15.5.18 + '@next/swc-darwin-arm64': 15.5.18 + '@next/swc-darwin-x64': 15.5.18 + '@next/swc-linux-arm64-gnu': 15.5.18 + '@next/swc-linux-arm64-musl': 15.5.18 + '@next/swc-linux-x64-gnu': 15.5.18 + '@next/swc-linux-x64-musl': 15.5.18 + '@next/swc-win32-arm64-msvc': 15.5.18 + '@next/swc-win32-x64-msvc': 15.5.18 babel-plugin-react-compiler: 1.0.0 sharp: 0.34.5 transitivePeerDependencies: - - "@babel/core" + - '@babel/core' - babel-plugin-macros optional: true @@ -10217,7 +9356,7 @@ snapshots: parse-json@5.2.0: dependencies: - "@babel/code-frame": 7.29.0 + '@babel/code-frame': 7.29.0 error-ex: 1.3.4 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 @@ -10643,34 +9782,34 @@ snapshots: sharp@0.34.5: dependencies: - "@img/colour": 1.1.0 + '@img/colour': 1.1.0 detect-libc: 2.1.2 semver: 7.8.5 optionalDependencies: - "@img/sharp-darwin-arm64": 0.34.5 - "@img/sharp-darwin-x64": 0.34.5 - "@img/sharp-libvips-darwin-arm64": 1.2.4 - "@img/sharp-libvips-darwin-x64": 1.2.4 - "@img/sharp-libvips-linux-arm": 1.2.4 - "@img/sharp-libvips-linux-arm64": 1.2.4 - "@img/sharp-libvips-linux-ppc64": 1.2.4 - "@img/sharp-libvips-linux-riscv64": 1.2.4 - "@img/sharp-libvips-linux-s390x": 1.2.4 - "@img/sharp-libvips-linux-x64": 1.2.4 - "@img/sharp-libvips-linuxmusl-arm64": 1.2.4 - "@img/sharp-libvips-linuxmusl-x64": 1.2.4 - "@img/sharp-linux-arm": 0.34.5 - "@img/sharp-linux-arm64": 0.34.5 - "@img/sharp-linux-ppc64": 0.34.5 - "@img/sharp-linux-riscv64": 0.34.5 - "@img/sharp-linux-s390x": 0.34.5 - "@img/sharp-linux-x64": 0.34.5 - "@img/sharp-linuxmusl-arm64": 0.34.5 - "@img/sharp-linuxmusl-x64": 0.34.5 - "@img/sharp-wasm32": 0.34.5 - "@img/sharp-win32-arm64": 0.34.5 - "@img/sharp-win32-ia32": 0.34.5 - "@img/sharp-win32-x64": 0.34.5 + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 optional: true shebang-command@2.0.0: @@ -10890,7 +10029,7 @@ snapshots: ts-morph@26.0.0: dependencies: - "@ts-morph/common": 0.27.0 + '@ts-morph/common': 0.27.0 code-block-writer: 13.0.3 tsconfig-paths@3.15.0: diff --git a/src/assets/svg/whatsapp.svg b/src/assets/svg/whatsapp.svg new file mode 100644 index 0000000..68a66f8 --- /dev/null +++ b/src/assets/svg/whatsapp.svg @@ -0,0 +1 @@ + diff --git a/src/components/dashboard-navigation.ts b/src/components/dashboard-navigation.ts index 8eb40fa..450e157 100644 --- a/src/components/dashboard-navigation.ts +++ b/src/components/dashboard-navigation.ts @@ -3,17 +3,20 @@ import { CircleQuestionMark, Database, FolderKanban, + FolderTree, Globe, LayoutDashboard, type LucideIcon, Settings, ShieldCheck, + Tags, Users, UsersRound, } from "lucide-react" import azureIcon from "@/assets/svg/azure.svg" import telegramIcon from "@/assets/svg/telegram.svg" +import whatsappIcon from "@/assets/svg/whatsapp.svg" export type DashboardNavigationItem = { title: string @@ -45,6 +48,12 @@ export const dashboardNavigation = [ { title: "Grants", url: "/dashboard/telegram/grants", icon: ShieldCheck }, ], }, + { + title: "WhatsApp", + icon: UsersRound, + iconSrc: whatsappIcon, + items: [{ title: "Groups", url: "/dashboard/whatsapp/groups", icon: Database }], + }, { title: "Azure", icon: UsersRound, @@ -63,6 +72,8 @@ export const dashboardNavigation = [ { title: "Associations", url: "/dashboard/web/associations", icon: Users }, { title: "Guides", url: "/dashboard/web/guides", icon: BookOpen }, { title: "FAQs", url: "/dashboard/web/faqs", icon: CircleQuestionMark }, + { title: "Group labels", url: "/dashboard/web/group-labels", icon: Tags }, + { title: "Categories", url: "/dashboard/web/groups-by-label", icon: FolderTree }, ], }, ] as const satisfies readonly DashboardNavigationCategory[] diff --git a/src/features/associations/associations.functions.ts b/src/features/associations/associations.functions.ts index a168fe9..c73ca7a 100644 --- a/src/features/associations/associations.functions.ts +++ b/src/features/associations/associations.functions.ts @@ -1,6 +1,6 @@ import { createServerFn } from "@tanstack/react-start" -import { adminMiddleware, writeAdminMiddleware } from "@/server/auth.middleware" +import { webAdminMiddleware, webWriteAdminMiddleware } from "@/server/auth.middleware" import { associationIdInput, @@ -10,7 +10,7 @@ import { } from "./associations.validation" export const getAssociations = createServerFn() - .middleware([adminMiddleware]) + .middleware([webAdminMiddleware]) .handler(({ context }) => context.backend.web.associations.getAllAssociations.query()) function associationFormData(data: ReturnType) { @@ -23,7 +23,7 @@ function associationFormData(data: ReturnType } export const createAssociation = createServerFn({ method: "POST" }) - .middleware([writeAdminMiddleware]) + .middleware([webWriteAdminMiddleware]) .validator(parseCreateAssociationForm) .handler(async ({ data, context }) => { const formData = associationFormData(data) @@ -32,7 +32,7 @@ export const createAssociation = createServerFn({ method: "POST" }) }) export const editAssociation = createServerFn({ method: "POST" }) - .middleware([writeAdminMiddleware]) + .middleware([webWriteAdminMiddleware]) .validator(parseEditAssociationForm) .handler(async ({ data, context }) => { const formData = associationFormData(data) @@ -44,7 +44,7 @@ export const editAssociation = createServerFn({ method: "POST" }) }) export const editAssociationLinks = createServerFn({ method: "POST" }) - .middleware([writeAdminMiddleware]) + .middleware([webWriteAdminMiddleware]) .validator(associationLinksInput) .handler(async ({ data, context }) => { const result = await context.backend.web.associations.editAssociationLinks.mutate({ @@ -56,7 +56,7 @@ export const editAssociationLinks = createServerFn({ method: "POST" }) }) export const deleteAssociation = createServerFn({ method: "POST" }) - .middleware([writeAdminMiddleware]) + .middleware([webWriteAdminMiddleware]) .validator(associationIdInput) .handler(async ({ data, context }) => { const result = await context.backend.web.associations.deleteAssociation.mutate(data) diff --git a/src/features/azure/group-membership.tsx b/src/features/azure/group-membership.tsx index c0786bc..fac465d 100644 --- a/src/features/azure/group-membership.tsx +++ b/src/features/azure/group-membership.tsx @@ -26,7 +26,15 @@ const MAX_VISIBLE_MEMBERS = 7 type MemberChoice = Pick -export function GroupMembership({ group, directoryMembers }: { group: AzureGroup; directoryMembers: AzureMember[] }) { +export function GroupMembership({ + group, + directoryMembers, + canWrite, +}: { + group: AzureGroup + directoryMembers: AzureMember[] + canWrite: boolean +}) { const sortedMembers = [...group.members].sort((a, b) => a.displayName.localeCompare(b.displayName)) const visibleMembers = sortedMembers.slice(0, MAX_VISIBLE_MEMBERS) const hiddenMembers = sortedMembers.slice(MAX_VISIBLE_MEMBERS) @@ -43,10 +51,12 @@ export function GroupMembership({ group, directoryMembers }: { group: AzureGroup ) : (

No members yet

)} -
- - -
+ {canWrite && ( +
+ + +
+ )} ) } diff --git a/src/features/azure/groups-page.tsx b/src/features/azure/groups-page.tsx index dd0077d..68b468e 100644 --- a/src/features/azure/groups-page.tsx +++ b/src/features/azure/groups-page.tsx @@ -17,15 +17,17 @@ import { GroupMembership } from "./group-membership" export function AzureGroupsPage({ groups, directoryMembers, + canWrite, }: { groups: AzureGroup[] directoryMembers: AzureMember[] + canWrite: boolean }) { return (
{groups.length ? ( - + ) : ( [...groups].sort((a, b) => a.displayName.localeCompare(b.displayName)), [groups]) const multiMemberGroups = sortedGroups.filter((group) => group.members.length > 1) const singleMemberGroups = sortedGroups.filter((group) => group.members.length <= 1) @@ -74,11 +84,17 @@ function GroupsList({ groups, directoryMembers }: { groups: AzureGroup[]; direct title="Groups with 2+ members" groups={multiMemberGroups} directoryMembers={directoryMembers} + canWrite={canWrite} defaultOpen /> )} {singleMemberGroups.length > 0 && ( - + )}
) @@ -88,11 +104,13 @@ function GroupSection({ title, groups, directoryMembers, + canWrite, defaultOpen = false, }: { title: string groups: AzureGroup[] directoryMembers: AzureMember[] + canWrite: boolean defaultOpen?: boolean }) { return ( @@ -115,7 +133,7 @@ function GroupSection({ {groups.map((group) => (
- +
))} @@ -124,7 +142,15 @@ function GroupSection({ ) } -function GroupRow({ group, directoryMembers }: { group: AzureGroup; directoryMembers: AzureMember[] }) { +function GroupRow({ + group, + directoryMembers, + canWrite, +}: { + group: AzureGroup + directoryMembers: AzureMember[] + canWrite: boolean +}) { return (
@@ -142,7 +168,7 @@ function GroupRow({ group, directoryMembers }: { group: AzureGroup; directoryMem
- +
) } diff --git a/src/features/azure/members-page.tsx b/src/features/azure/members-page.tsx index b886746..ff754fc 100644 --- a/src/features/azure/members-page.tsx +++ b/src/features/azure/members-page.tsx @@ -27,7 +27,7 @@ function memberFilterFrom(value: Value): MemberFilter { return result.success ? result.data : { query: "", membersOnly: false } } -export function AzureMembersPage({ initialMembers }: { initialMembers: AzureMember[] }) { +export function AzureMembersPage({ initialMembers, canWrite }: { initialMembers: AzureMember[]; canWrite: boolean }) { const router = useRouter() const [dialog, setDialog] = useState(null) const [members, setMembers] = useState(initialMembers) @@ -107,22 +107,26 @@ export function AzureMembersPage({ initialMembers }: { initialMembers: AzureMemb ), }), - memberColumnHelper.display({ - id: "actions", - header: "", - cell: ({ row }) => ( - - ), - }), + ...(canWrite + ? [ + memberColumnHelper.display({ + id: "actions", + header: "", + cell: ({ row }) => ( + + ), + }), + ] + : []), ]) - }, []) + }, [canWrite]) const table = useAppTable({ key: "azure-members", columns, @@ -162,9 +166,11 @@ export function AzureMembersPage({ initialMembers }: { initialMembers: AzureMemb table.setPageIndex(0) }} action={ - + canWrite ? ( + + ) : undefined } > + ))} + + ) : ( +
void submit(event)}> + + + Name + setSegment(event.target.value)} + placeholder="sub-category" + maxLength={GROUP_LABEL_MAX} + autoFocus + required + /> + + {!isValidLabelSegment(trimmed) && trimmed && ( +

Use a plain name, without dots or URL separators.

+ )} + {error &&

{error}

} + + + + +
+ )} + + + ) +} diff --git a/src/features/group-labels/add-tag-dialog.tsx b/src/features/group-labels/add-tag-dialog.tsx new file mode 100644 index 0000000..928bdc4 --- /dev/null +++ b/src/features/group-labels/add-tag-dialog.tsx @@ -0,0 +1,117 @@ +import { useRouter } from "@tanstack/react-router" +import { useServerFn } from "@tanstack/react-start" +import { LoaderCircle } from "lucide-react" +import { useId, useState } from "react" +import { toast } from "sonner" + +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Field, FieldLabel } from "@/components/ui/field" +import { Input } from "@/components/ui/input" +import { errorMessage } from "@/lib/errors" + +import { DEFAULT_GROUP_LABEL_COLOR, GROUP_LABEL_MAX } from "./group-labels.constants" +import { createGroupLabel } from "./group-labels.functions" +import { isReservedCategoryRoot, isValidLabelSegment } from "./label-tree" + +/** Creates a flat attribute tag (e.g. a language or campus facet). Tags never nest, unlike a category. */ +export function AddTagDialog({ open, onOpenChange }: { open: boolean; onOpenChange: (open: boolean) => void }) { + const router = useRouter() + const createGroupLabelFn = useServerFn(createGroupLabel) + const [name, setName] = useState("") + const [pending, setPending] = useState(false) + const [error, setError] = useState("") + const nameId = useId() + + const trimmed = name.trim() + const reserved = isReservedCategoryRoot(trimmed) + const canSave = isValidLabelSegment(trimmed) && !reserved + + function reset() { + setName("") + setError("") + } + + async function submit(event: React.FormEvent) { + event.preventDefault() + if (!canSave || pending) return + setPending(true) + setError("") + try { + await createGroupLabelFn({ data: { label: trimmed, color: DEFAULT_GROUP_LABEL_COLOR, description: "" } }) + toast.success(`Tag "${trimmed}" created.`) + onOpenChange(false) + reset() + await router.invalidate({ sync: true }) + } catch (cause) { + console.error(cause) + setError(errorMessage(cause, `"${trimmed}" could not be created. Try a different name.`)) + } finally { + setPending(false) + } + } + + return ( + { + if (pending) return + onOpenChange(nextOpen) + if (!nextOpen) reset() + }} + > + + + Add tag + + A flat attribute, like a language or campus. Tags aren't nested — for a browsable category, use + "Add category" instead. + + +
void submit(event)}> + + Name + setName(event.target.value)} + placeholder="e.g. Italian, Bovisa" + maxLength={GROUP_LABEL_MAX} + autoFocus + required + /> + + {!isValidLabelSegment(trimmed) && trimmed && ( +

Use a plain name, without dots or URL separators.

+ )} + {reserved &&

This name is reserved for a category.

} + {error &&

{error}

} + + + + +
+
+
+ ) +} diff --git a/src/features/group-labels/group-label-badges.tsx b/src/features/group-labels/group-label-badges.tsx new file mode 100644 index 0000000..2746016 --- /dev/null +++ b/src/features/group-labels/group-label-badges.tsx @@ -0,0 +1,53 @@ +import { Link } from "@tanstack/react-router" + +import { cn } from "@/lib/utils" + +import { getGroupLabelColor } from "./group-labels.constants" +import { LabelDot } from "./label-dot" +import { formatLabelCompact, formatLabelSegment, isCategoryLabel, labelPathToUrlSegments } from "./label-tree" +import type { GroupLabel } from "./types" + +/** + * Renders a group's labels split by kind, instead of one undifferentiated row of identical badges: its flat + * tags as solid color-coded pills (their color is what makes them scannable, e.g. a language or campus at a + * glance), and its category as a lighter, clickable outline chip that jumps straight to that category's group + * list — the color belongs to the tag, not to a position in the hierarchy. + */ +export function GroupLabelBadges({ labels }: { labels: GroupLabel[] }) { + if (!labels.length) return No labels + + const categories = labels.filter((label) => isCategoryLabel(label.label)) + const tags = labels.filter((label) => !isCategoryLabel(label.label)) + + return ( +
event.stopPropagation()}> + {categories.map((label) => { + const url: string = `/dashboard/web/groups-by-label/${labelPathToUrlSegments(label.label).join("/")}` + return ( + + + {formatLabelCompact(label.label)} + + ) + })} + {tags.map((label) => { + const swatch = getGroupLabelColor(label.color) + return ( + + {formatLabelSegment(label.label)} + + ) + })} +
+ ) +} diff --git a/src/features/group-labels/group-label-card.tsx b/src/features/group-labels/group-label-card.tsx new file mode 100644 index 0000000..b59c311 --- /dev/null +++ b/src/features/group-labels/group-label-card.tsx @@ -0,0 +1,262 @@ +import { FolderPlus, LoaderCircle, MoreVertical, Pencil, PencilLine, Save, Trash2, X } from "lucide-react" +import { useState } from "react" + +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogMedia, + AlertDialogTitle, +} from "@/components/ui/alert-dialog" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Card } from "@/components/ui/card" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { Input } from "@/components/ui/input" +import { AddChildLabelDialog } from "@/features/groups-by-label/add-child-label-dialog" +import { cn } from "@/lib/utils" + +import { GroupLabelColorPicker } from "./group-label-color-picker" +import { GROUP_LABEL_DESCRIPTION_MAX, getGroupLabelColor } from "./group-labels.constants" +import { formatLabelBreadcrumb, formatLabelSegment } from "./label-tree" +import { RenameLabelDialog } from "./rename-label-dialog" +import type { GroupLabel, GroupLabelEditValues } from "./types" + +type GroupLabelCardProps = { + groupLabel: GroupLabel + /** The full label list, used to find this label's descendants when renaming (they get re-pathed too). */ + allLabels: GroupLabel[] + /** Rendered before everything else in the row — the tree's expand/collapse chevron, when this node has children. */ + leading?: React.ReactNode + /** Off for the two fixed category roots — the whole category tree depends on their exact name. */ + allowRename?: boolean + /** Off for flat tags, which never nest. */ + allowChildren?: boolean + onDelete: () => Promise + onSave: (values: GroupLabelEditValues) => Promise +} + +export function GroupLabelCard({ + groupLabel, + allLabels, + leading, + allowRename = true, + allowChildren = true, + onDelete, + onSave, +}: GroupLabelCardProps) { + const [color, setColor] = useState(groupLabel.color) + const [description, setDescription] = useState(groupLabel.description ?? "") + const [editing, setEditing] = useState(false) + const [saving, setSaving] = useState(false) + const [deleting, setDeleting] = useState(false) + const [deleteOpen, setDeleteOpen] = useState(false) + const [renameOpen, setRenameOpen] = useState(false) + const [addChildOpen, setAddChildOpen] = useState(false) + + const segments = groupLabel.label.split(".") + const segment = segments[segments.length - 1] || groupLabel.label + const displaySegment = formatLabelSegment(segment) + + function resetFields() { + setColor(groupLabel.color) + setDescription(groupLabel.description ?? "") + } + + function cancelEdit() { + resetFields() + setEditing(false) + } + + async function save() { + if (saving) return + setSaving(true) + try { + const saved = await onSave({ color, description: description.trim() }) + if (saved) setEditing(false) + } finally { + setSaving(false) + } + } + + async function remove() { + setDeleting(true) + try { + if (await onDelete()) setDeleteOpen(false) + } finally { + setDeleting(false) + } + } + + const swatch = getGroupLabelColor(editing ? color : groupLabel.color) + const affectedLabels = allLabels.filter( + (label) => label.label === groupLabel.label || label.label.startsWith(`${groupLabel.label}.`) + ) + + return ( + <> + + {leading} + + {editing ? ( + <> +
+ + + {displaySegment} + +
+ setDescription(event.target.value)} + className="min-w-0 flex-1 bg-background" + maxLength={GROUP_LABEL_DESCRIPTION_MAX} + placeholder="What kind of groups does this label apply to?" + /> + + ) : ( + <> + + {displaySegment} + +

+ {groupLabel.description || No description} +

+ + )} + +
+ {editing ? ( + <> + + + + ) : ( + <> + {(allowRename || allowChildren) && ( + + }> + + More actions for {displaySegment} + + + + {allowRename && ( + setRenameOpen(true)}> + Rename + + )} + {allowChildren && ( + setAddChildOpen(true)}> + Add sub-category + + )} + + + + )} + + + + )} +
+
+ + {allowRename && ( + + )} + {allowChildren && ( + + )} + + !deleting && setDeleteOpen(open)}> + + + + + + Delete label + + Are you sure you want to delete {formatLabelBreadcrumb(groupLabel.label)}? This action + cannot be undone. + + + + setDeleteOpen(false)}> + Cancel + + void remove()}> + {deleting && } Delete + + + + + + ) +} diff --git a/src/features/group-labels/group-label-color-picker.tsx b/src/features/group-labels/group-label-color-picker.tsx new file mode 100644 index 0000000..756a453 --- /dev/null +++ b/src/features/group-labels/group-label-color-picker.tsx @@ -0,0 +1,36 @@ +import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { cn } from "@/lib/utils" + +import { getGroupLabelColor, GROUP_LABEL_COLORS } from "./group-labels.constants" + +export function GroupLabelColorPicker({ value, onChange }: { value: string; onChange: (color: string) => void }) { + const swatch = getGroupLabelColor(value) + + return ( + + ) +} diff --git a/src/features/group-labels/group-label-tree-row.tsx b/src/features/group-labels/group-label-tree-row.tsx new file mode 100644 index 0000000..680cddf --- /dev/null +++ b/src/features/group-labels/group-label-tree-row.tsx @@ -0,0 +1,141 @@ +import { ChevronRight, FolderPlus, MoreVertical, PencilLine } from "lucide-react" +import { useState } from "react" + +import { Button } from "@/components/ui/button" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { AddChildLabelDialog } from "@/features/groups-by-label/add-child-label-dialog" +import { cn } from "@/lib/utils" + +import { GroupLabelCard } from "./group-label-card" +import { formatLabelBreadcrumb, formatLabelSegment, isReservedCategoryRoot, type LabelTreeNode } from "./label-tree" +import { RenameLabelDialog } from "./rename-label-dialog" +import type { GroupLabel, GroupLabelEditValues } from "./types" + +/** Recursively renders a category-hierarchy node: a full editable row when it's a real label, or a plain + * grouping row (rename/add-child only) when it's just an implied ancestor with no label of its own. */ +export function GroupLabelTreeRow({ + node, + depth, + forceExpanded, + allLabels, + onDelete, + onSave, +}: { + node: LabelTreeNode + depth: number + forceExpanded: boolean + allLabels: GroupLabel[] + onDelete: (label: GroupLabel) => Promise + onSave: (label: GroupLabel, values: GroupLabelEditValues) => Promise +}) { + const label = node.label + const hasChildren = node.children.length > 0 + const isRoot = depth === 0 + // The two fixed category roots (Didattica, Extra) can't be renamed — the whole tree keys off their exact name. + const allowRename = !(isRoot && isReservedCategoryRoot(node.path)) + const [manualExpanded, setManualExpanded] = useState(false) + const expanded = forceExpanded || manualExpanded + const [renameOpen, setRenameOpen] = useState(false) + const [addChildOpen, setAddChildOpen] = useState(false) + + const chevron = ( + + ) + + return ( +
0 && "ml-4 border-l border-border/60 pl-3")}> + {label ? ( + onDelete(label)} + onSave={(values) => onSave(label, values)} + /> + ) : ( +
+ {chevron} + + {formatLabelSegment(node.segment)} + + {!isRoot && ( + No details set for this level yet + )} + + }> + + More actions for {formatLabelBreadcrumb(node.path)} + + + + {allowRename && ( + setRenameOpen(true)}> + Rename + + )} + setAddChildOpen(true)}> + Add sub-category + + + + + {allowRename && ( + label.label === node.path || label.label.startsWith(`${node.path}.`) + )} + open={renameOpen} + onOpenChange={setRenameOpen} + /> + )} + +
+ )} + {hasChildren && expanded && ( +
+ {node.children.map((child) => ( + + ))} +
+ )} +
+ ) +} diff --git a/src/features/group-labels/group-labels-dialog.tsx b/src/features/group-labels/group-labels-dialog.tsx new file mode 100644 index 0000000..ac7f208 --- /dev/null +++ b/src/features/group-labels/group-labels-dialog.tsx @@ -0,0 +1,128 @@ +import { useServerFn } from "@tanstack/react-start" +import { LoaderCircle } from "lucide-react" +import { useEffect, useState } from "react" +import { toast } from "sonner" + +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { isSameGroupLabel } from "@/features/group-labels/group-labels.constants" +import { tagGroup, untagGroup } from "@/features/group-labels/group-labels.functions" +import { LabelTreeSelector } from "@/features/group-labels/label-tree-selector" +import type { TgGroupLabel } from "@/lib/api/types" +import { errorMessage } from "@/lib/errors" + +/** Works for both Telegram and WhatsApp groups; the platform prevents ID collisions. */ +export function GroupLabelsDialog({ + group, + allLabels, + currentLabels, + onClose, + onSaved, +}: { + group: { id: number; title: string; type: "tg" | "wa" } | null + allLabels: TgGroupLabel[] + currentLabels: TgGroupLabel[] + onClose: () => void + onSaved: () => Promise +}) { + const tagGroupFn = useServerFn(tagGroup) + const untagGroupFn = useServerFn(untagGroup) + const [selected, setSelected] = useState(currentLabels) + const [pending, setPending] = useState(false) + const [error, setError] = useState("") + const open = group !== null + + // Resets the working selection only when a different group is opened, not on every relations refresh + // (currentLabels is intentionally excluded: it gets a new array identity on every parent render) — and + // keyed on the group's id rather than the `group` object itself, since every call site constructs a new + // `{ id, title }` literal inline on every render, which would otherwise re-fire this effect (discarding + // any in-progress, unsaved selection) on every unrelated re-render while the dialog is open. + useEffect(() => { + if (group) { + setSelected(currentLabels) + setError("") + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [group?.id]) + + const added = selected.filter((label) => !currentLabels.some((current) => isSameGroupLabel(current, label))) + const removed = currentLabels.filter((label) => !selected.some((next) => isSameGroupLabel(next, label))) + const dirty = added.length > 0 || removed.length > 0 + + function toggleManyLabels(labels: TgGroupLabel[], select: boolean) { + setSelected((current) => { + if (select) { + const toAdd = labels.filter((label) => !current.some((existing) => isSameGroupLabel(existing, label))) + return [...current, ...toAdd] + } + return current.filter((existing) => !labels.some((label) => isSameGroupLabel(existing, label))) + }) + } + + async function save() { + if (!group || !dirty || pending) return + setPending(true) + setError("") + try { + const results = await Promise.allSettled([ + ...added.map((label) => tagGroupFn({ data: { groupId: group.id, type: group.type, label: label.label } })), + ...removed.map((label) => untagGroupFn({ data: { groupId: group.id, type: group.type, label: label.label } })), + ]) + const failed = results.filter((result) => result.status === "rejected").length + if (failed > 0) { + // Some of these may have already applied even though others failed — refresh so the underlying + // data (and this dialog, next time it opens) reflects what's actually saved, instead of silently + // implying nothing happened. + await onSaved() + setError( + `${failed} of ${results.length} label change(s) couldn't be saved — some may have already applied. Check the group's labels and try again.` + ) + return + } + toast.success(`Labels updated for ${group.title}.`) + onClose() + await onSaved() + } catch (cause) { + console.error(cause) + setError(errorMessage(cause, "The group labels could not be updated. Check your permissions and try again.")) + } finally { + setPending(false) + } + } + + return ( + !pending && !nextOpen && onClose()}> + + + Edit labels + {group ? `Choose the labels that apply to ${group.title}.` : null} + +
+

Labels

+ {allLabels.length ? ( + + ) : ( +

No labels have been created yet.

+ )} +
+ {error &&

{error}

} + + + + +
+
+ ) +} diff --git a/src/features/group-labels/group-labels-page.tsx b/src/features/group-labels/group-labels-page.tsx new file mode 100644 index 0000000..d6f8c00 --- /dev/null +++ b/src/features/group-labels/group-labels-page.tsx @@ -0,0 +1,133 @@ +import { FolderTree, Plus, Tags } from "lucide-react" +import { useMemo, useState } from "react" + +import { DataToolbar } from "@/components/data-toolbar" +import { EmptyState } from "@/components/empty-state" +import { Button } from "@/components/ui/button" + +import { AddCategoryDialog } from "./add-category-dialog" +import { AddTagDialog } from "./add-tag-dialog" +import { GroupLabelCard } from "./group-label-card" +import { GroupLabelTreeRow } from "./group-label-tree-row" +import { + buildCategoryRootTree, + filterFlatLabels, + filterLabelTree, + isCategoryLabel, + type LabelTreeNode, +} from "./label-tree" +import type { GroupLabel } from "./types" +import { useGroupLabelRows } from "./use-group-label-rows" + +function countRealLabels(nodes: LabelTreeNode[]): number { + let count = 0 + for (const node of nodes) { + if (node.label) count += 1 + count += countRealLabels(node.children) + } + return count +} + +export function GroupLabelsPage({ loadedGroupLabels }: { loadedGroupLabels: GroupLabel[] }) { + const { labels, saveGroupLabel, removeGroupLabel } = useGroupLabelRows(loadedGroupLabels) + const [query, setQuery] = useState("") + const [addCategoryOpen, setAddCategoryOpen] = useState(false) + const [addTagOpen, setAddTagOpen] = useState(false) + + const categoryLabels = useMemo(() => labels.filter((label) => isCategoryLabel(label.label)), [labels]) + const tagLabels = useMemo(() => labels.filter((label) => !isCategoryLabel(label.label)), [labels]) + + const categoryTree = useMemo(() => buildCategoryRootTree(categoryLabels), [categoryLabels]) + const filteredCategoryTree = useMemo(() => filterLabelTree(categoryTree, query), [categoryTree, query]) + const filteredTags = useMemo(() => filterFlatLabels(tagLabels, query), [tagLabels, query]) + + const isSearching = Boolean(query.trim()) + const matchCount = countRealLabels(filteredCategoryTree) + filteredTags.length + + return ( +
+ + + +
+ } + /> + +
+

Categories

+

+ A browsable hierarchy — drill down from Didattica or Extra to organize groups by course, year, or type. +

+ {filteredCategoryTree.length ? ( +
+ {filteredCategoryTree.map((node) => ( + + ))} +
+ ) : ( + + )} +
+ +
+

Tags

+

+ Flat attributes, like a language or campus, that don't belong to the category hierarchy. +

+ {filteredTags.length ? ( +
+ {filteredTags.map((label) => ( + removeGroupLabel(label)} + onSave={(values) => saveGroupLabel(label, values)} + /> + ))} +
+ ) : ( + setAddTagOpen(true)}>Add first tag : undefined} + /> + )} +
+ + + + + ) +} diff --git a/src/features/group-labels/group-labels.constants.ts b/src/features/group-labels/group-labels.constants.ts new file mode 100644 index 0000000..6602f0d --- /dev/null +++ b/src/features/group-labels/group-labels.constants.ts @@ -0,0 +1,101 @@ +import type { CSSProperties } from "react" + +export const GROUP_LABEL_MAX = 128 +export const GROUP_LABEL_DESCRIPTION_MAX = 500 + +export const GROUP_LABEL_COLORS = [ + { + hex: "#64748b", + label: "Gray", + dot: "bg-slate-500", + badge: "bg-slate-500/10 text-slate-700 dark:bg-slate-400/15 dark:text-slate-300", + }, + { + hex: "#ef4444", + label: "Red", + dot: "bg-red-500", + badge: "bg-red-500/10 text-red-700 dark:bg-red-400/15 dark:text-red-300", + }, + { + hex: "#f97316", + label: "Orange", + dot: "bg-orange-500", + badge: "bg-orange-500/10 text-orange-700 dark:bg-orange-400/15 dark:text-orange-300", + }, + { + hex: "#f59e0b", + label: "Amber", + dot: "bg-amber-500", + badge: "bg-amber-500/10 text-amber-700 dark:bg-amber-400/15 dark:text-amber-300", + }, + { + hex: "#22c55e", + label: "Green", + dot: "bg-green-500", + badge: "bg-green-500/10 text-green-700 dark:bg-green-400/15 dark:text-green-300", + }, + { + hex: "#14b8a6", + label: "Teal", + dot: "bg-teal-500", + badge: "bg-teal-500/10 text-teal-700 dark:bg-teal-400/15 dark:text-teal-300", + }, + { + hex: "#3b82f6", + label: "Blue", + dot: "bg-blue-500", + badge: "bg-blue-500/10 text-blue-700 dark:bg-blue-400/15 dark:text-blue-300", + }, + { + hex: "#6366f1", + label: "Indigo", + dot: "bg-indigo-500", + badge: "bg-indigo-500/10 text-indigo-700 dark:bg-indigo-400/15 dark:text-indigo-300", + }, + { + hex: "#a855f7", + label: "Purple", + dot: "bg-purple-500", + badge: "bg-purple-500/10 text-purple-700 dark:bg-purple-400/15 dark:text-purple-300", + }, + { + hex: "#ec4899", + label: "Pink", + dot: "bg-pink-500", + badge: "bg-pink-500/10 text-pink-700 dark:bg-pink-400/15 dark:text-pink-300", + }, +] as const + +export const DEFAULT_GROUP_LABEL_COLOR: string = GROUP_LABEL_COLORS[6].hex // Blue + +export function isGroupLabelHexColor(value: string): boolean { + return /^#[0-9A-Fa-f]{6}$/.test(value) +} + +export function isSameGroupLabel(a: { label: string }, b: { label: string }): boolean { + return a.label === b.label +} + +type GroupLabelSwatch = { + hex: string + label: string + dotClassName: string + dotStyle?: CSSProperties + badgeClassName: string + badgeStyle?: CSSProperties +} + +/** Falls back to an outline swatch styled with the raw hex, for colors set outside this curated palette. */ +export function getGroupLabelColor(hex: string): GroupLabelSwatch { + const match = GROUP_LABEL_COLORS.find((color) => color.hex.toLowerCase() === hex.toLowerCase()) + if (match) return { hex: match.hex, label: match.label, dotClassName: match.dot, badgeClassName: match.badge } + + return { + hex, + label: "Custom", + dotClassName: "", + dotStyle: { backgroundColor: hex }, + badgeClassName: "border bg-transparent", + badgeStyle: { borderColor: hex, color: hex }, + } +} diff --git a/src/features/group-labels/group-labels.functions.ts b/src/features/group-labels/group-labels.functions.ts new file mode 100644 index 0000000..58a5bff --- /dev/null +++ b/src/features/group-labels/group-labels.functions.ts @@ -0,0 +1,99 @@ +import { createServerFn } from "@tanstack/react-start" +import { z } from "zod" + +import { webAdminMiddleware, webWriteAdminMiddleware } from "@/server/auth.middleware" + +import { + createGroupLabelInput, + editGroupLabelInput, + groupLabelIdentifierInput, + renameGroupLabelInput, +} from "./group-labels.validation" + +export const listGroupLabels = createServerFn() + .middleware([webAdminMiddleware]) + .handler(({ context }) => context.backend.tg.groupLabels.getAll.query()) + +/** All groups (Telegram + WhatsApp) with their labels already resolved. */ +export const listGroupsWithLabels = createServerFn() + .middleware([webAdminMiddleware]) + .handler(({ context }) => context.backend.groups.search.getAll.query()) + +/** Platform is required because Telegram and WhatsApp group IDs may collide. */ +const groupLabelTagInput = z.object({ + groupId: z.number().int(), + type: z.enum(["tg", "wa"]), + label: z.string().min(1).max(128), +}) + +/** The web-only category view needs both platform lists without broadening their dashboard functions. */ +export const listGroupsForLabels = createServerFn() + .middleware([webAdminMiddleware]) + .handler(async ({ context }) => { + const [tgGroups, waGroups] = await Promise.all([ + context.backend.tg.groups.getAll.query(), + context.backend.wa.groups.getAll.query(), + ]) + return { tgGroups, waGroups } + }) + +export const tagGroup = createServerFn({ method: "POST" }) + .middleware([webWriteAdminMiddleware]) + .validator(groupLabelTagInput) + .handler(({ data, context }) => context.backend.groups.labels.tagGroup.mutate(data)) + +export const untagGroup = createServerFn({ method: "POST" }) + .middleware([webWriteAdminMiddleware]) + .validator(groupLabelTagInput) + .handler(({ data, context }) => context.backend.groups.labels.untagGroup.mutate(data)) + +export const createGroupLabel = createServerFn({ method: "POST" }) + .middleware([webWriteAdminMiddleware]) + .validator(createGroupLabelInput) + .handler(async ({ data, context }) => { + const [created] = await context.backend.tg.groupLabels.create.mutate({ + label: data.label, + description: data.description, + color: data.color, + createdBy: context.telegramId, + }) + if (!created) throw new Error("The label could not be created.") + return created + }) + +export const editGroupLabel = createServerFn({ method: "POST" }) + .middleware([webWriteAdminMiddleware]) + .validator(editGroupLabelInput) + .handler(async ({ data, context }) => { + const [updated] = await context.backend.tg.groupLabels.modify.mutate({ + label: data.label, + description: data.description, + color: data.color, + updatedBy: context.telegramId, + }) + if (!updated) throw new Error("NOT_FOUND") + return updated + }) + +export const renameGroupLabel = createServerFn({ method: "POST" }) + .middleware([webWriteAdminMiddleware]) + .validator(renameGroupLabelInput) + .handler(async ({ data, context }) => { + const [renamed] = await context.backend.tg.groupLabels.modify.mutate({ + label: data.label, + newLabel: data.newLabel, + description: data.description, + color: data.color, + updatedBy: context.telegramId, + }) + if (!renamed) throw new Error("NOT_FOUND") + return renamed + }) + +export const deleteGroupLabel = createServerFn({ method: "POST" }) + .middleware([webWriteAdminMiddleware]) + .validator(groupLabelIdentifierInput) + .handler(async ({ data, context }) => { + await context.backend.tg.groupLabels.delete.mutate({ label: data.label }) + return { error: null } + }) diff --git a/src/features/group-labels/group-labels.validation.ts b/src/features/group-labels/group-labels.validation.ts new file mode 100644 index 0000000..17ad048 --- /dev/null +++ b/src/features/group-labels/group-labels.validation.ts @@ -0,0 +1,64 @@ +import { z } from "zod" + +import { errorHasZodField, errorMessage } from "@/lib/errors" + +import { GROUP_LABEL_DESCRIPTION_MAX, GROUP_LABEL_MAX } from "./group-labels.constants" +import { isCategoryLabel, isValidLabelSegment } from "./label-tree" + +const label = z.string().trim().min(1).max(GROUP_LABEL_MAX) +const color = z.string().regex(/^#[0-9A-Fa-f]{6}$/) +const description = z.string().trim().max(GROUP_LABEL_DESCRIPTION_MAX) + +/** + * The fixed category root a path is nested under, e.g. "didattica" for "didattica.design.triennale"; `null` + * for a bare tag (tags have no root to preserve). Deliberately *not* "everything but the last segment": a + * cascading rename re-paths every descendant at once (e.g. renaming "didattica.design" to "didattica.web" also + * turns "didattica.design.triennale" into "didattica.web.triennale"), so only the outermost root stays the + * same across the whole batch — the immediate parent segment shifts for every descendant by construction. + */ +function categoryRoot(value: string): string | null { + const dotIndex = value.indexOf(".") + return dotIndex === -1 ? null : value.slice(0, dotIndex) +} + +/** + * The server-side backstop for the label shapes the admin UI can produce (see CATEGORY_ROOTS in + * label-tree.ts): a flat tag with no dots, a fixed category root used to hold groups directly, or a path nested + * under one of those roots. The tag-creation UI still reserves the root names; allowing them here lets the + * "Add group" flow materialize `extra` or `didattica` the first time a group is assigned directly to that root. + * Only applied where a *new* path is being written into existence — fields that merely identify an already-existing + * row (edit, delete, or the "from" side of a rename) stay unrestricted so a pre-existing row is always manageable. + */ +const newLabelPath = label.refine( + (value) => { + const segments = value.split(".") + if (segments.some((segment) => !isValidLabelSegment(segment))) return false + return segments.length === 1 || isCategoryLabel(value) + }, + { message: "Use a plain tag name, a category root, or nest a category under an existing root." } +) + +export const createGroupLabelInput = z.object({ label: newLabelPath, color, description }) +export const editGroupLabelInput = z.object({ label, color, description }) +export const renameGroupLabelInput = z + .object({ label, newLabel: newLabelPath, color, description }) + .refine((data) => categoryRoot(data.label) === categoryRoot(data.newLabel), { + message: "A rename can't move a label to a different category root, or turn a tag into a category (or back).", + path: ["newLabel"], + }) +export const groupLabelIdentifierInput = z.object({ label }) + +export function groupLabelSaveErrorMessage(cause: unknown) { + if (errorHasZodField(cause, "newLabel")) { + return "A rename can't move a label to a different category root, or turn a tag into a category (or back)." + } + if (errorHasZodField(cause, "label")) { + return `Enter a valid label: a plain tag name, a category root, or a category nested under an existing root (max ${GROUP_LABEL_MAX} characters).` + } + if (errorHasZodField(cause, "color")) return "Choose a valid color." + if (errorHasZodField(cause, "description")) { + return `Enter a description no longer than ${GROUP_LABEL_DESCRIPTION_MAX} characters.` + } + if (/duplicate|unique/i.test(errorMessage(cause, ""))) return "A label with this name already exists." + return "The label could not be saved. Check your permissions and try again." +} diff --git a/src/features/group-labels/label-dot.tsx b/src/features/group-labels/label-dot.tsx new file mode 100644 index 0000000..6a085c6 --- /dev/null +++ b/src/features/group-labels/label-dot.tsx @@ -0,0 +1,8 @@ +import { cn } from "@/lib/utils" + +import { getGroupLabelColor } from "./group-labels.constants" + +export function LabelDot({ color, className }: { color: string; className?: string }) { + const swatch = getGroupLabelColor(color) + return +} diff --git a/src/features/group-labels/label-tree-selector.tsx b/src/features/group-labels/label-tree-selector.tsx new file mode 100644 index 0000000..406bf00 --- /dev/null +++ b/src/features/group-labels/label-tree-selector.tsx @@ -0,0 +1,238 @@ +import { Check, ChevronRight, Minus, X } from "lucide-react" +import { useMemo, useState } from "react" + +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { cn } from "@/lib/utils" + +import { getGroupLabelColor } from "./group-labels.constants" +import { LabelDot } from "./label-dot" +import { + buildLabelTree, + collectSubtreeLabels, + filterFlatLabels, + formatLabelBreadcrumb, + formatLabelChip, + formatLabelSegment, + isCategoryLabel, + type LabelTreeNode, +} from "./label-tree" +import type { GroupLabel } from "./types" + +function LabelTreeSelectorNode({ + node, + isSelected, + onToggleMany, + depth, +}: { + node: LabelTreeNode + isSelected: (label: GroupLabel) => boolean + onToggleMany: (labels: GroupLabel[], select: boolean) => void + depth: number +}) { + const [expanded, setExpanded] = useState(false) + const hasChildren = node.children.length > 0 + const subtreeLabels = useMemo(() => collectSubtreeLabels(node), [node]) + const selectedCount = subtreeLabels.filter((label) => isSelected(label)).length + const allSelected = subtreeLabels.length > 0 && selectedCount === subtreeLabels.length + const someSelected = selectedCount > 0 && !allSelected + + return ( +
0 && "ml-4 border-l border-border/60 pl-2")}> +
+ + +
+ {expanded && + node.children.map((child) => ( + + ))} +
+ ) +} + +/** A single matching category, found by search — its full breadcrumb stands in for tree position, so picking it + * is a straight click instead of expanding down to it level by level. */ +function CategorySearchResult({ + label, + isSelected, + onToggleMany, +}: { + label: GroupLabel + isSelected: (label: GroupLabel) => boolean + onToggleMany: (labels: GroupLabel[], select: boolean) => void +}) { + const checked = isSelected(label) + return ( + + ) +} + +/** Lets an admin pick labels for a group: a browsable category tree, plus flat tag chips — no dotted paths shown. */ +export function LabelTreeSelector({ + allLabels, + selected, + onToggleMany, +}: { + allLabels: GroupLabel[] + selected: GroupLabel[] + onToggleMany: (labels: GroupLabel[], select: boolean) => void +}) { + const [query, setQuery] = useState("") + const categoryLabels = useMemo(() => allLabels.filter((label) => isCategoryLabel(label.label)), [allLabels]) + const tagLabels = useMemo(() => allLabels.filter((label) => !isCategoryLabel(label.label)), [allLabels]) + const tree = useMemo(() => buildLabelTree(categoryLabels), [categoryLabels]) + const isSearching = Boolean(query.trim()) + // Searching a hierarchy by expanding one branch at a time is slow, unlike picking a tag — so a search instead + // flattens straight to matching categories (with their breadcrumb for context), skipping the tree entirely. + const matchingCategories = useMemo(() => filterFlatLabels(categoryLabels, query), [categoryLabels, query]) + const visibleTags = useMemo( + () => (isSearching ? filterFlatLabels(tagLabels, query) : tagLabels), + [tagLabels, query, isSearching] + ) + const isSelected = (label: GroupLabel) => selected.some((current) => current.label === label.label) + + return ( +
+ setQuery(event.target.value)} + className="h-9" + /> + {selected.length > 0 && ( +
+ {selected.map((label) => { + const swatch = getGroupLabelColor(label.color) + return ( + + ) + })} +
+ )} +
+ {isSearching + ? matchingCategories.length > 0 && ( +
+

+ Categories +

+ {matchingCategories.map((label) => ( + + ))} +
+ ) + : tree.length > 0 && ( +
+

+ Categories +

+ {tree.map((node) => ( + + ))} +
+ )} + {visibleTags.length > 0 && ( +
+

Tags

+
+ {visibleTags.map((label) => { + const checked = isSelected(label) + const swatch = getGroupLabelColor(label.color) + return ( + + ) + })} +
+
+ )} + {(isSearching ? !matchingCategories.length : !tree.length) && !visibleTags.length && ( +

No matching labels

+ )} +
+
+ ) +} diff --git a/src/features/group-labels/label-tree.ts b/src/features/group-labels/label-tree.ts new file mode 100644 index 0000000..e1bdadb --- /dev/null +++ b/src/features/group-labels/label-tree.ts @@ -0,0 +1,192 @@ +import type { GroupLabel } from "./types" + +/** + * The two fixed, structural containers for the browsable category hierarchy (school/level/course/year, or the + * extra-groups branch) — e.g. "didattica.design.triennale" or "extra.affitti". Everything else (language/campus + * facets like "ita", "eng", "bovisa") is a flat attribute tag attached to individual groups, not part of this + * tree. These roots are deliberately not admin-creatable: without a backend "kind" column there's no way to tell + * an empty category-root-in-waiting apart from a flat tag, so the distinction is made *by construction* — a new + * category is always created nested under one of these two, a new tag is always created bare. + */ +export const CATEGORY_ROOTS = ["didattica", "extra"] + +export function isCategoryLabel(label: string): boolean { + return CATEGORY_ROOTS.some((root) => label === root || label.startsWith(`${root}.`)) +} + +/** True when `name` collides with a fixed category root — reserved so a tag or rename can never shadow one. */ +export function isReservedCategoryRoot(name: string): boolean { + const normalized = name.trim().toLocaleLowerCase() + return CATEGORY_ROOTS.some((root) => root === normalized) +} + +/** A path segment is embedded directly in the groups-by-label route, so it must not contain URL separators. */ +export function isValidLabelSegment(segment: string): boolean { + return segment.length > 0 && !/[./\\?#%]/.test(segment) +} + +/** Humanizes a single path segment for display, e.g. "primo-anno" -> "Primo Anno". */ +export function formatLabelSegment(segment: string): string { + return segment + .split(/[-_]+/) + .filter(Boolean) + .map((word) => word.charAt(0).toLocaleUpperCase() + word.slice(1)) + .join(" ") +} + +/** Renders a dotted path as a readable breadcrumb, e.g. "didattica.design" -> "Didattica › Design". */ +export function formatLabelBreadcrumb(path: string): string { + return labelPathToUrlSegments(path).map(formatLabelSegment).join(" › ") +} + +/** + * Humanizes a category path for a compact chip: its last two segments, e.g. "didattica.design.triennale" -> + * "Design › Triennale". A single trailing segment alone is often low-information (e.g. an academic year like + * "26-27" tacked onto the end of a category), so two segments give a chip enough context to be useful on its own. + */ +export function formatLabelCompact(path: string): string { + return labelPathToUrlSegments(path).slice(-2).map(formatLabelSegment).join(" › ") +} + +/** Picks the right compact display for any label, category or tag, without the caller needing to branch. */ +export function formatLabelChip(path: string): string { + return isCategoryLabel(path) ? formatLabelCompact(path) : formatLabelSegment(path) +} + +export type LabelTreeNode = { + segment: string + path: string + label: GroupLabel | null + children: LabelTreeNode[] +} + +/** Splits each label on "." to derive a hierarchy; segments with no matching label are pure grouping nodes. */ +export function buildLabelTree(labels: GroupLabel[]): LabelTreeNode[] { + const nodesByPath = new Map() + const roots: LabelTreeNode[] = [] + + function getOrCreateNode(segments: string[]): LabelTreeNode { + const path = segments.join(".") + const existing = nodesByPath.get(path) + if (existing) return existing + + const node: LabelTreeNode = { segment: segments[segments.length - 1] ?? "", path, label: null, children: [] } + nodesByPath.set(path, node) + + if (segments.length === 1) roots.push(node) + else getOrCreateNode(segments.slice(0, -1)).children.push(node) + + return node + } + + for (const label of labels) { + const segments = label.label.split(".").filter((segment: string) => segment.length > 0) + if (!segments.length) continue + getOrCreateNode(segments).label = label + } + + function sortNodes(nodes: LabelTreeNode[]) { + nodes.sort((a, b) => a.segment.localeCompare(b.segment)) + for (const node of nodes) sortNodes(node.children) + } + sortNodes(roots) + + return roots +} + +/** Like `buildLabelTree`, but guarantees both fixed category roots always appear (as empty placeholders if + * they have no labels yet), so the management page always has somewhere to add the first category under. */ +export function buildCategoryRootTree(categoryLabels: GroupLabel[]): LabelTreeNode[] { + const nodesByPath = new Map(buildLabelTree(categoryLabels).map((node) => [node.path, node])) + return CATEGORY_ROOTS.map((root) => nodesByPath.get(root) ?? { segment: root, path: root, label: null, children: [] }) +} + +/** Finds the node at `path` within a tree, walking down one segment at a time instead of searching blindly. */ +export function findLabelTreeNode(nodes: LabelTreeNode[], path: string): LabelTreeNode | undefined { + for (const node of nodes) { + if (node.path === path) return node + if (path.startsWith(`${node.path}.`)) return findLabelTreeNode(node.children, path) + } + return undefined +} + +/** Converts a hierarchy path ("informatica.triennale") to its sidebar URL segments. */ +export function labelPathToUrlSegments(path: string): string[] { + return path.split(".").filter((segment) => segment.length > 0) +} + +/** Converts sidebar URL segments back to a dotted hierarchy path — the inverse of `labelPathToUrlSegments`, + * so a malformed URL (trailing/double slash) can't produce a path with an empty segment (e.g. a trailing dot). */ +export function urlSegmentsToLabelPath(segments: string[]): string { + return segments.filter((segment) => segment.length > 0).join(".") +} + +/** A group matches a branch when one of its labels is exactly `path`, or nested under it (e.g. "informatica.triennale.primo" under "informatica"). */ +export function matchesLabelBranch(groupLabels: GroupLabel[], path: string): boolean { + return groupLabels.some((label) => label.label === path || label.label.startsWith(`${path}.`)) +} + +/** A group has this exact category — unlike `matchesLabelBranch`, a group nested one level deeper doesn't count. */ +export function hasExactLabel(groupLabels: GroupLabel[], path: string): boolean { + return groupLabels.some((label) => label.label === path) +} + +/** + * Resolves each group's label strings (from the cross-platform `groups.search.getAll` result) to full label + * objects, for groups of the given platform only — Telegram and WhatsApp group ids are independent sequences + * that could otherwise collide. + */ +export function buildLabelsByGroupId( + labels: GroupLabel[], + groupsWithLabels: { id: number; type: "tg" | "wa"; labels: string[] }[], + type: "tg" | "wa" +): Map { + const labelsByName = new Map(labels.map((label) => [label.label, label])) + const map = new Map() + for (const group of groupsWithLabels) { + if (group.type !== type) continue + const resolved = group.labels.map((name) => labelsByName.get(name)).filter((label) => label != null) + if (resolved.length) map.set(group.id, resolved) + } + return map +} + +/** Every real label in a node's own subtree (itself, if it has one, plus all of its descendants). */ +export function collectSubtreeLabels(node: LabelTreeNode): GroupLabel[] { + const labels: GroupLabel[] = [] + if (node.label) labels.push(node.label) + for (const child of node.children) labels.push(...collectSubtreeLabels(child)) + return labels +} + +function nodeSelfMatches(node: LabelTreeNode, normalizedQuery: string): boolean { + if (node.segment.toLocaleLowerCase().includes(normalizedQuery)) return true + if (!node.label) return false + return [node.label.label, node.label.description ?? ""].some((value) => + value.toLocaleLowerCase().includes(normalizedQuery) + ) +} + +/** Keeps a node when it matches, or when any descendant does, so a matching leaf stays reachable through its ancestors. */ +export function filterLabelTree(nodes: LabelTreeNode[], query: string): LabelTreeNode[] { + const normalizedQuery = query.trim().toLocaleLowerCase() + if (!normalizedQuery) return nodes + + const result: LabelTreeNode[] = [] + for (const node of nodes) { + const filteredChildren = filterLabelTree(node.children, query) + if (nodeSelfMatches(node, normalizedQuery) || filteredChildren.length) { + result.push({ ...node, children: filteredChildren }) + } + } + return result +} + +/** Filters a flat list of tags (no hierarchy) by name or description — the flat-list counterpart of `filterLabelTree`. */ +export function filterFlatLabels(labels: GroupLabel[], query: string): GroupLabel[] { + const normalizedQuery = query.trim().toLocaleLowerCase() + if (!normalizedQuery) return labels + return labels.filter((label) => + [label.label, label.description ?? ""].some((value) => value.toLocaleLowerCase().includes(normalizedQuery)) + ) +} diff --git a/src/features/group-labels/rename-label-dialog.tsx b/src/features/group-labels/rename-label-dialog.tsx new file mode 100644 index 0000000..7d6a737 --- /dev/null +++ b/src/features/group-labels/rename-label-dialog.tsx @@ -0,0 +1,169 @@ +import { useRouter } from "@tanstack/react-router" +import { useServerFn } from "@tanstack/react-start" +import { LoaderCircle } from "lucide-react" +import { useEffect, useId, useState } from "react" +import { toast } from "sonner" + +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Field, FieldLabel } from "@/components/ui/field" +import { Input } from "@/components/ui/input" + +import { GROUP_LABEL_MAX } from "./group-labels.constants" +import { renameGroupLabel } from "./group-labels.functions" +import { groupLabelSaveErrorMessage } from "./group-labels.validation" +import { formatLabelBreadcrumb, isReservedCategoryRoot, isValidLabelSegment } from "./label-tree" +import type { GroupLabel } from "./types" + +export function RenameLabelDialog({ + path, + segment, + affectedLabels, + open, + onOpenChange, +}: { + /** The full dotted path being renamed, e.g. "informatica.magistrale" — may be a pure grouping node with no label of its own. */ + path: string + /** The last segment of `path`, e.g. "magistrale". */ + segment: string + /** Every real label at `path` or nested under it (self, if any, plus all descendants) — all get re-pathed. */ + affectedLabels: GroupLabel[] + open: boolean + onOpenChange: (open: boolean) => void +}) { + const router = useRouter() + const renameGroupLabelFn = useServerFn(renameGroupLabel) + const [newSegment, setNewSegment] = useState(segment) + const [pending, setPending] = useState(false) + const [error, setError] = useState("") + const inputId = useId() + + useEffect(() => { + if (open) { + setNewSegment(segment) + setError("") + } + }, [open, segment]) + + const trimmed = newSegment.trim() + // No "." before the segment when it's a top-level node (path === segment). + const parentPrefix = path.length > segment.length ? path.slice(0, path.length - segment.length - 1) : "" + // Only a bare top-level rename (a tag, since the two category roots never reach this dialog) could collide + // with a reserved root name — a nested rename can't, since it'd still be dotted. + const reserved = !parentPrefix && isReservedCategoryRoot(trimmed) + const canSave = isValidLabelSegment(trimmed) && trimmed !== segment && !reserved + const newPath = parentPrefix ? `${parentPrefix}.${trimmed}` : trimmed + + async function submit(event: React.FormEvent) { + event.preventDefault() + if (!canSave || pending) return + setPending(true) + setError("") + // The backend renames each affected label one at a time with no cross-label transaction, so a rename + // spanning several nested labels can fail partway and leave the tree split between the old and new path. + // There's no atomic batch endpoint to call instead (would need a backend change, out of scope here), so + // this settles every rename first and, on any failure, best-effort renames the successful ones back — + // "all or nothing" from the admin's point of view even without a real DB transaction underneath. + const renames = affectedLabels.map((label) => ({ + label, + newLabel: newPath + label.label.slice(path.length), + })) + try { + const results = await Promise.allSettled( + renames.map(({ label, newLabel }) => + renameGroupLabelFn({ + data: { label: label.label, newLabel, color: label.color, description: label.description ?? "" }, + }) + ) + ) + const anyFailed = results.some((result) => result.status === "rejected") + if (!anyFailed) { + toast.success( + affectedLabels.length > 1 + ? `Renamed "${formatLabelBreadcrumb(path)}" and ${affectedLabels.length - 1} nested label(s) to "${formatLabelBreadcrumb(newPath)}".` + : `Renamed to "${formatLabelBreadcrumb(newPath)}".` + ) + onOpenChange(false) + // Both current callers (the tree row and the flat card) live on the labels management page itself, so + // there's nowhere to navigate to — just refresh so the renamed node shows up immediately. + await router.invalidate({ sync: true }) + return + } + + const succeeded = renames.filter((_, index) => results[index]?.status === "fulfilled") + const rollbackResults = await Promise.allSettled( + succeeded.map(({ label, newLabel }) => + renameGroupLabelFn({ + data: { label: newLabel, newLabel: label.label, color: label.color, description: label.description ?? "" }, + }) + ) + ) + const rollbackFailed = rollbackResults.some((result) => result.status === "rejected") + setError( + rollbackFailed + ? "The rename failed partway and some labels couldn't be rolled back automatically — check the category tree for leftover names." + : "The rename couldn't be completed and was rolled back. Check your permissions and try again." + ) + await router.invalidate({ sync: true }) + } catch (cause) { + console.error(cause) + setError(groupLabelSaveErrorMessage(cause)) + } finally { + setPending(false) + } + } + + return ( + !pending && onOpenChange(nextOpen)}> + + + Rename category + + {affectedLabels.length > 1 + ? `Renames "${formatLabelBreadcrumb(path)}" and the ${affectedLabels.length - 1} label(s) nested under it, keeping their colors, descriptions, and tagged groups.` + : `Renaming "${formatLabelBreadcrumb(path)}" keeps its color, description, and any groups already tagged with it.`} + + +
void submit(event)}> + + Name + setNewSegment(event.target.value)} + maxLength={GROUP_LABEL_MAX} + autoFocus + required + /> + + {!isValidLabelSegment(trimmed) && trimmed && ( +

Use a plain name, without dots or URL separators.

+ )} + {reserved &&

This name is reserved for a category.

} + {trimmed && !trimmed.includes(".") && trimmed !== segment && ( +

+ Will become {newPath} +

+ )} + {error &&

{error}

} + + + + +
+
+
+ ) +} diff --git a/src/features/group-labels/types.ts b/src/features/group-labels/types.ts new file mode 100644 index 0000000..25a548e --- /dev/null +++ b/src/features/group-labels/types.ts @@ -0,0 +1,10 @@ +import type { TgGroupLabel } from "@/lib/api/types" + +export type GroupLabel = TgGroupLabel + +/** Color and description are the only fields ever edited inline — the name is set once at creation and only + * ever changed through the dedicated rename flow (which cascades to descendants for categories). */ +export type GroupLabelEditValues = { + color: string + description: string +} diff --git a/src/features/group-labels/use-group-label-rows.ts b/src/features/group-labels/use-group-label-rows.ts new file mode 100644 index 0000000..01d03df --- /dev/null +++ b/src/features/group-labels/use-group-label-rows.ts @@ -0,0 +1,62 @@ +import { useRouter } from "@tanstack/react-router" +import { useServerFn } from "@tanstack/react-start" +import { useEffect, useState } from "react" +import { toast } from "sonner" + +import { errorMessage } from "@/lib/errors" + +import { deleteGroupLabel, editGroupLabel } from "./group-labels.functions" +import { groupLabelSaveErrorMessage } from "./group-labels.validation" +import type { GroupLabel, GroupLabelEditValues } from "./types" + +export function useGroupLabelRows(loadedGroupLabels: GroupLabel[]) { + const router = useRouter() + const editGroupLabelFn = useServerFn(editGroupLabel) + const deleteGroupLabelFn = useServerFn(deleteGroupLabel) + const [labels, setLabels] = useState(loadedGroupLabels) + + useEffect(() => { + setLabels(loadedGroupLabels) + }, [loadedGroupLabels]) + + async function refresh() { + try { + await router.invalidate({ sync: true }) + } catch (error) { + console.error(error) + toast.warning("Your change was saved, but the label list could not be refreshed.") + } + } + + async function saveGroupLabel(groupLabel: GroupLabel, values: GroupLabelEditValues) { + try { + const saved = await editGroupLabelFn({ + data: { label: groupLabel.label, color: values.color, description: values.description }, + }) + setLabels((current) => current.map((current_) => (current_.label === saved.label ? saved : current_))) + toast.success("Label updated") + void refresh() + return true + } catch (cause) { + console.error(cause) + toast.error(groupLabelSaveErrorMessage(cause)) + return false + } + } + + async function removeGroupLabel(groupLabel: GroupLabel) { + try { + await deleteGroupLabelFn({ data: { label: groupLabel.label } }) + setLabels((current) => current.filter((current_) => current_.label !== groupLabel.label)) + toast.success("Label deleted") + void refresh() + return true + } catch (cause) { + console.error(cause) + toast.error(errorMessage(cause, "The label could not be deleted. Check your permissions and try again.")) + return false + } + } + + return { labels, saveGroupLabel, removeGroupLabel } +} diff --git a/src/features/groups-by-label/add-child-label-dialog.tsx b/src/features/groups-by-label/add-child-label-dialog.tsx new file mode 100644 index 0000000..450578f --- /dev/null +++ b/src/features/groups-by-label/add-child-label-dialog.tsx @@ -0,0 +1,149 @@ +import { useRouter } from "@tanstack/react-router" +import { useServerFn } from "@tanstack/react-start" +import { LoaderCircle, Plus } from "lucide-react" +import { useId, useState } from "react" +import { toast } from "sonner" + +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { Field, FieldLabel } from "@/components/ui/field" +import { Input } from "@/components/ui/input" +import { DEFAULT_GROUP_LABEL_COLOR } from "@/features/group-labels/group-labels.constants" +import { createGroupLabel } from "@/features/group-labels/group-labels.functions" +import { formatLabelBreadcrumb, isValidLabelSegment, labelPathToUrlSegments } from "@/features/group-labels/label-tree" +import { errorMessage } from "@/lib/errors" + +export function AddChildLabelDialog({ + path, + open: controlledOpen, + onOpenChange: controlledOnOpenChange, + navigateOnSuccess = true, +}: { + path: string + /** Pass both to drive the dialog externally (e.g. from a sidebar menu item) instead of rendering its own button. */ + open?: boolean + onOpenChange?: (open: boolean) => void + /** Off when opened from the labels management page, which should stay put — only the browse page (where + * jumping into the newly created branch makes sense) wants the post-create navigation. */ + navigateOnSuccess?: boolean +}) { + const router = useRouter() + const createGroupLabelFn = useServerFn(createGroupLabel) + const isControlled = controlledOpen !== undefined + const [uncontrolledOpen, setUncontrolledOpen] = useState(false) + const open = isControlled ? controlledOpen : uncontrolledOpen + const setOpen = controlledOnOpenChange ?? setUncontrolledOpen + const [segment, setSegment] = useState("") + const [pending, setPending] = useState(false) + const [error, setError] = useState("") + const segmentId = useId() + + const trimmed = segment.trim() + const canSave = isValidLabelSegment(trimmed) + + function reset() { + setSegment("") + setError("") + } + + async function submit(event: React.FormEvent) { + event.preventDefault() + if (!canSave || pending) return + setPending(true) + setError("") + const childPath = `${path}.${trimmed}` + try { + await createGroupLabelFn({ data: { label: childPath, color: DEFAULT_GROUP_LABEL_COLOR, description: "" } }) + toast.success(`"${childPath}" created.`) + setOpen(false) + reset() + if (navigateOnSuccess) { + await router.navigate({ to: `/dashboard/web/groups-by-label/${labelPathToUrlSegments(childPath).join("/")}` }) + } + // Force a refresh so the new node appears wherever it's listed (this page's own tree, or the + // browse page just navigated into) without waiting on a background revalidation. + await router.invalidate({ sync: true }) + } catch (cause) { + console.error(cause) + setError(errorMessage(cause, `"${childPath}" could not be created. Try a different name.`)) + } finally { + setPending(false) + } + } + + return ( + { + if (pending) return + setOpen(nextOpen) + if (!nextOpen) reset() + }} + > + {!isControlled && ( + }> + Add category + + )} + + + Add sub-category + + Creates a new category nested under{" "} + {formatLabelBreadcrumb(path)}. + + +
void submit(event)}> + + Name + setSegment(event.target.value)} + placeholder="sub-category" + autoFocus + required + /> + + {isValidLabelSegment(trimmed) && ( +

+ Will be created as{" "} + + {path}.{trimmed} + +

+ )} + {!isValidLabelSegment(trimmed) && trimmed && ( +

Use a plain name, without dots or URL separators.

+ )} + {error &&

{error}

} + + + + +
+
+
+ ) +} diff --git a/src/features/groups-by-label/add-group-to-label-dialog.tsx b/src/features/groups-by-label/add-group-to-label-dialog.tsx new file mode 100644 index 0000000..79b41aa --- /dev/null +++ b/src/features/groups-by-label/add-group-to-label-dialog.tsx @@ -0,0 +1,397 @@ +import { useRouter } from "@tanstack/react-router" +import { useServerFn } from "@tanstack/react-start" +import { ArrowLeft, Check, LoaderCircle, Plus, Search, X } from "lucide-react" +import { useState } from "react" +import { toast } from "sonner" + +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { Input } from "@/components/ui/input" +import { DEFAULT_GROUP_LABEL_COLOR } from "@/features/group-labels/group-labels.constants" +import { createGroupLabel, tagGroup } from "@/features/group-labels/group-labels.functions" +import { formatLabelBreadcrumb } from "@/features/group-labels/label-tree" +import { createWhatsappGroup } from "@/features/whatsapp/groups.functions" +import { WhatsappGroupFields } from "@/features/whatsapp/whatsapp-group-fields" +import { isValidWhatsappInviteLink } from "@/features/whatsapp/whatsapp.validation" +import type { TgGroup, TgGroupLabel, WaGroup } from "@/lib/api/types" +import { errorMessage } from "@/lib/errors" +import { cn } from "@/lib/utils" + +type Step = "choose" | "new" | "existing" +type Platform = "telegram" | "whatsapp" + +export function AddGroupToLabelDialog({ + path, + labelExists, + tgGroups, + waGroups, + tgLabelsByGroupId, + waLabelsByGroupId, +}: { + path: string + labelExists: boolean + tgGroups: TgGroup[] + waGroups: WaGroup[] + tgLabelsByGroupId: Map + waLabelsByGroupId: Map +}) { + const router = useRouter() + const createGroupLabelFn = useServerFn(createGroupLabel) + const createWhatsappGroupFn = useServerFn(createWhatsappGroup) + const tagGroupFn = useServerFn(tagGroup) + + const [open, setOpen] = useState(false) + const [step, setStep] = useState("choose") + const [platform, setPlatform] = useState(null) + const [groupQuery, setGroupQuery] = useState("") + const [selectedTgGroups, setSelectedTgGroups] = useState([]) + const [selectedWaGroups, setSelectedWaGroups] = useState([]) + const [title, setTitle] = useState("") + const [link, setLink] = useState("") + const [pending, setPending] = useState(false) + const [error, setError] = useState("") + + const availableTgGroups = tgGroups.filter( + (g) => !(tgLabelsByGroupId.get(g.telegramId) ?? []).some((l) => l.label === path) + ) + const availableWaGroups = waGroups.filter((g) => !(waLabelsByGroupId.get(g.id) ?? []).some((l) => l.label === path)) + const normalizedGroupQuery = groupQuery.trim().toLocaleLowerCase() + const filteredTgGroups = normalizedGroupQuery + ? availableTgGroups.filter((g) => g.title.toLocaleLowerCase().includes(normalizedGroupQuery)) + : availableTgGroups + const filteredWaGroups = normalizedGroupQuery + ? availableWaGroups.filter((g) => g.title.toLocaleLowerCase().includes(normalizedGroupQuery)) + : availableWaGroups + + function reset() { + setStep("choose") + setPlatform(null) + setGroupQuery("") + setSelectedTgGroups([]) + setSelectedWaGroups([]) + setTitle("") + setLink("") + setError("") + } + + function closeDialog() { + setOpen(false) + reset() + } + + function toggleTgGroup(group: TgGroup) { + setSelectedTgGroups((current) => + current.some((g) => g.telegramId === group.telegramId) + ? current.filter((g) => g.telegramId !== group.telegramId) + : [...current, group] + ) + } + + function toggleWaGroup(group: WaGroup) { + setSelectedWaGroups((current) => + current.some((g) => g.id === group.id) ? current.filter((g) => g.id !== group.id) : [...current, group] + ) + } + + async function ensureLabelExists() { + if (!labelExists) { + await createGroupLabelFn({ data: { label: path, color: DEFAULT_GROUP_LABEL_COLOR, description: "" } }) + } + } + + async function submitNew(event: React.FormEvent) { + event.preventDefault() + if (!title.trim() || !isValidWhatsappInviteLink(link.trim()) || pending) return + setPending(true) + setError("") + try { + await ensureLabelExists() + const created = await createWhatsappGroupFn({ data: { title: title.trim(), link: link.trim() } }) + try { + await tagGroupFn({ data: { groupId: created.id, type: "wa", label: path } }) + } catch (tagCause) { + console.error(tagCause) + toast.warning( + `${title.trim()} was added, but could not be labeled "${formatLabelBreadcrumb(path)}". Assign it manually.` + ) + closeDialog() + try { + await router.invalidate({ sync: true }) + } catch (refreshCause) { + console.error(refreshCause) + toast.warning("The group was added, but the latest group data could not be refreshed.") + } + return + } + toast.success(`${title.trim()} added and labeled "${formatLabelBreadcrumb(path)}".`) + closeDialog() + try { + await router.invalidate({ sync: true }) + } catch (refreshCause) { + console.error(refreshCause) + toast.warning("The group was added and labeled, but the latest group data could not be refreshed.") + } + } catch (cause) { + console.error(cause) + setError(errorMessage(cause, "The group could not be created. Check the details and try again.")) + } finally { + setPending(false) + } + } + + async function submitExisting() { + if (pending) return + const groupsToTag = platform === "telegram" ? selectedTgGroups : platform === "whatsapp" ? selectedWaGroups : [] + if (!groupsToTag.length) return + setPending(true) + setError("") + try { + await ensureLabelExists() + const results = await Promise.allSettled( + groupsToTag.map((group) => + tagGroupFn({ + data: { + groupId: "telegramId" in group ? group.telegramId : group.id, + type: "telegramId" in group ? "tg" : "wa", + label: path, + }, + }) + ) + ) + const failed = results.filter((result) => result.status === "rejected").length + if (failed > 0) { + // Some groups may have already been tagged even though others failed — refresh so the underlying + // data reflects what's actually saved, instead of silently implying nothing happened. + await router.invalidate({ sync: true }) + setError( + failed === groupsToTag.length + ? "The groups could not be labeled. Check your permissions and try again." + : `${failed} of ${groupsToTag.length} group(s) couldn't be labeled — the rest were. Check your permissions and try again.` + ) + return + } + toast.success( + groupsToTag.length === 1 + ? `${groupsToTag[0].title} labeled "${formatLabelBreadcrumb(path)}".` + : `${groupsToTag.length} groups labeled "${formatLabelBreadcrumb(path)}".` + ) + closeDialog() + await router.invalidate({ sync: true }) + } catch (cause) { + console.error(cause) + setError(errorMessage(cause, "The groups could not be labeled. Check your permissions and try again.")) + } finally { + setPending(false) + } + } + + const selectedExistingCount = + platform === "telegram" ? selectedTgGroups.length : platform === "whatsapp" ? selectedWaGroups.length : 0 + const canSubmitExisting = selectedExistingCount > 0 + + return ( + { + if (pending) return + if (nextOpen) setOpen(true) + else closeDialog() + }} + > + }> + Add group + + + + Add group to "{formatLabelBreadcrumb(path)}" + + {step === "choose" && "Create a brand new group, or categorize groups that already exist."} + {step === "new" && "There's no bot managing WhatsApp groups yet, so this is just a manual record."} + {step === "existing" && + (platform ? "Pick one or more groups to categorize." : "Which platform are the groups on?")} + + + + {step === "choose" && ( +
+ + +
+ )} + + {step === "new" && ( +
void submitNew(event)}> + + + {error &&

{error}

} + + + + + + )} + + {step === "existing" && ( +
+ + + {!platform ? ( +
+ + +
+ ) : ( +
+ setGroupQuery(event.target.value)} + className="h-9" + /> + {selectedExistingCount > 0 && ( +
+ + {selectedExistingCount} selected + +
+ {(platform === "telegram" ? selectedTgGroups : selectedWaGroups).map((group) => ( + + ))} +
+
+ )} +
+ {platform === "telegram" ? ( + filteredTgGroups.length ? ( + filteredTgGroups.map((group) => { + const checked = selectedTgGroups.some((g) => g.telegramId === group.telegramId) + return ( + + ) + }) + ) : ( +

No matching groups

+ ) + ) : filteredWaGroups.length ? ( + filteredWaGroups.map((group) => { + const checked = selectedWaGroups.some((g) => g.id === group.id) + return ( + + ) + }) + ) : ( +

No matching groups

+ )} +
+
+ )} + + {error &&

{error}

} + + + + +
+ )} +
+
+ ) +} diff --git a/src/features/groups-by-label/combined-groups-table.tsx b/src/features/groups-by-label/combined-groups-table.tsx new file mode 100644 index 0000000..29b9d33 --- /dev/null +++ b/src/features/groups-by-label/combined-groups-table.tsx @@ -0,0 +1,324 @@ +import { useRouter } from "@tanstack/react-router" +import { useServerFn } from "@tanstack/react-start" +import type { Column } from "@tanstack/react-table" +import { + ArrowDown, + ArrowUp, + ChevronsUpDown, + ExternalLink, + Eye, + EyeOff, + LoaderCircle, + MessageCircleMore, +} from "lucide-react" +import { useMemo, useState } from "react" +import { toast } from "sonner" + +import { EmptyState } from "@/components/empty-state" +import { Pagination } from "@/components/pagination" +import { Alert, AlertDescription } from "@/components/ui/alert" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { DataTableHead, Table, TableBody, TableCell, TableHeader, TableRow, TableSurface } from "@/components/ui/table" +import { GroupLabelBadges } from "@/features/group-labels/group-label-badges" +import { GroupLabelsDialog } from "@/features/group-labels/group-labels-dialog" +import { setGroupVisibility } from "@/features/telegram/groups.functions" +import { LeaveGroupDialog } from "@/features/telegram/leave-group-dialog" +import { CreateEditGroupDialog } from "@/features/whatsapp/create-edit-group-dialog" +import { DeleteGroupDialog } from "@/features/whatsapp/delete-group-dialog" +import type { TgGroup, TgGroupLabel, WaGroup } from "@/lib/api/types" +import { createAppColumnHelper, type dashboardFeatures, useAppTable } from "@/lib/table" +import { cn } from "@/lib/utils" + +type CombinedGroupRowBase = { + key: string + title: string + tag: string | null + link: string | null + labels: TgGroupLabel[] +} + +export type CombinedGroupRow = + | (CombinedGroupRowBase & { platform: "telegram"; group: TgGroup }) + | (CombinedGroupRowBase & { platform: "whatsapp"; group: WaGroup }) + +const groupColumnHelper = createAppColumnHelper() + +export function CombinedGroupsTable({ + rows, + allLabels, + emptyTitle, + emptyText, +}: { + rows: CombinedGroupRow[] + allLabels: TgGroupLabel[] + emptyTitle: string + emptyText: string +}) { + const router = useRouter() + const setGroupVisibilityFn = useServerFn(setGroupVisibility) + const [visibilityOverrides, setVisibilityOverrides] = useState>({}) + const [updatingId, setUpdatingId] = useState(null) + const [mutationError, setMutationError] = useState("") + const [refreshError, setRefreshError] = useState("") + const [editingKey, setEditingKey] = useState(null) + + const displayRows = rows.map((row) => + row.platform === "telegram" + ? { ...row, group: { ...row.group, hide: visibilityOverrides[row.group.telegramId] ?? row.group.hide } } + : row + ) + const editingRow = editingKey ? (displayRows.find((row) => row.key === editingKey) ?? null) : null + + async function toggleVisibility(group: TgGroup) { + if (updatingId !== null) return + const hide = !group.hide + setUpdatingId(group.telegramId) + setMutationError("") + setVisibilityOverrides((current) => ({ ...current, [group.telegramId]: hide })) + + try { + await setGroupVisibilityFn({ data: { telegramId: group.telegramId, hide } }) + toast.success(`${group.title} is now ${hide ? "hidden" : "visible"}.`) + try { + await router.invalidate({ sync: true }) + setRefreshError("") + setVisibilityOverrides((current) => { + const { [group.telegramId]: _removed, ...remaining } = current + return remaining + }) + } catch (error) { + console.error(error) + setRefreshError("The visibility was updated, but the latest group data could not be refreshed.") + } + } catch (error) { + console.error(error) + setVisibilityOverrides((current) => { + const { [group.telegramId]: _removed, ...remaining } = current + return remaining + }) + setMutationError("The visibility setting could not be updated. Check your permissions and try again.") + } finally { + setUpdatingId(null) + } + } + + const columns = useMemo(() => { + const sortableHeader = ( + label: string, + column: Pick, "getIsSorted" | "getToggleSortingHandler"> + ) => { + const sorted = column.getIsSorted() + const Icon = !sorted ? ChevronsUpDown : sorted === "asc" ? ArrowUp : ArrowDown + return ( + + ) + } + + return groupColumnHelper.columns([ + groupColumnHelper.accessor("title", { + header: ({ column }) => sortableHeader("Group", column), + cell: ({ row }) => ( +
+ + + +
+ {row.original.title} + {row.original.platform} +
+
+ ), + }), + groupColumnHelper.accessor("tag", { + header: ({ column }) => sortableHeader("Tag", column), + cell: ({ getValue }) => + getValue() ? ( + + @{getValue()} + + ) : ( + Not set + ), + }), + groupColumnHelper.accessor("labels", { + header: "Labels", + cell: ({ getValue }) => , + }), + groupColumnHelper.accessor("link", { + header: "Invite", + cell: ({ getValue }) => { + const link = getValue() + return link ? ( + event.stopPropagation()} + > + + Open invite link + + ) : ( + Not shared + ) + }, + }), + groupColumnHelper.display({ + id: "actions", + header: "", + cell: ({ row }) => { + if (row.original.platform === "whatsapp") { + const group = row.original.group + return ( +
event.stopPropagation()} className="flex items-center gap-1.5"> + + +
+ ) + } + + const group = row.original.group + const pending = updatingId === group.telegramId + const visible = !group.hide + return ( +
event.stopPropagation()} className="flex items-center gap-1.5"> + + +
+ ) + }, + }), + ]) + }, [updatingId]) + + const table = useAppTable({ + key: "groups-by-label-combined", + columns, + data: displayRows, + initialState: { sorting: [{ id: "title", desc: false }], pagination: { pageIndex: 0, pageSize: 20 } }, + }) + + const editingGroupRef = + editingRow?.platform === "telegram" + ? { id: editingRow.group.telegramId, title: editingRow.title, type: "tg" as const } + : editingRow?.platform === "whatsapp" + ? { id: editingRow.group.id, title: editingRow.title, type: "wa" as const } + : null + const editingCurrentLabels = editingRow?.labels ?? [] + + return ( + <> + {mutationError && ( + + {mutationError} + + )} + {refreshError && ( + + {refreshError} + + )} + {displayRows.length ? ( + <> + + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder ? null : } + + ))} + + ))} + + + {table.getRowModel().rows.map((row) => ( + setEditingKey(row.original.key)} + onKeyDown={(event) => { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault() + setEditingKey(row.original.key) + } + }} + > + {row.getAllCells().map((cell) => ( + + + + ))} + + ))} + +
+
+ table.setPageIndex(page - 1)} + onPageSizeChange={(pageSize) => table.setPageSize(pageSize)} + /> + + ) : ( + + )} + setEditingKey(null)} + onSaved={async () => { + try { + await router.invalidate({ sync: true }) + } catch (error) { + console.error(error) + toast.warning("The labels were saved, but the group list could not be refreshed.") + } + }} + /> + + ) +} diff --git a/src/features/groups-by-label/groups-by-label-page.tsx b/src/features/groups-by-label/groups-by-label-page.tsx new file mode 100644 index 0000000..5a9704e --- /dev/null +++ b/src/features/groups-by-label/groups-by-label-page.tsx @@ -0,0 +1,199 @@ +import { Link } from "@tanstack/react-router" +import { ArrowLeft, ChevronRight } from "lucide-react" +import { useMemo, useState } from "react" + +import { DataToolbar } from "@/components/data-toolbar" +import { Button } from "@/components/ui/button" +import { + buildCategoryRootTree, + buildLabelsByGroupId, + findLabelTreeNode, + formatLabelBreadcrumb, + formatLabelSegment, + hasExactLabel, + isCategoryLabel, + labelPathToUrlSegments, +} from "@/features/group-labels/label-tree" +import { AddChildLabelDialog } from "@/features/groups-by-label/add-child-label-dialog" +import { AddGroupToLabelDialog } from "@/features/groups-by-label/add-group-to-label-dialog" +import { CombinedGroupsTable, type CombinedGroupRow } from "@/features/groups-by-label/combined-groups-table" +import type { GroupWithLabels, TgGroup, TgGroupLabel, WaGroup } from "@/lib/api/types" + +export function GroupsByLabelPage({ + path, + loadedTgGroups, + loadedGroupLabels, + loadedGroupsWithLabels, + loadedWaGroups, +}: { + path: string + loadedTgGroups: TgGroup[] + loadedGroupLabels: TgGroupLabel[] + loadedGroupsWithLabels: GroupWithLabels[] + loadedWaGroups: WaGroup[] +}) { + const [query, setQuery] = useState("") + + // Kept as two separate maps (not merged into one) since Telegram and WhatsApp group ids are independent + // sequences that could otherwise collide. + const tgLabelsByGroupId = useMemo( + () => buildLabelsByGroupId(loadedGroupLabels, loadedGroupsWithLabels, "tg"), + [loadedGroupLabels, loadedGroupsWithLabels] + ) + const waLabelsByGroupId = useMemo( + () => buildLabelsByGroupId(loadedGroupLabels, loadedGroupsWithLabels, "wa"), + [loadedGroupLabels, loadedGroupsWithLabels] + ) + + // Only groups tagged with this exact category — a level below shows up as a sub-category to click into, not + // mixed into this list, so the admin always knows precisely where a group is filed. + const branchRows = useMemo(() => { + const tgRows: CombinedGroupRow[] = loadedTgGroups + .filter((group) => hasExactLabel(tgLabelsByGroupId.get(group.telegramId) ?? [], path)) + .map((group) => ({ + key: `telegram-${group.telegramId}`, + platform: "telegram", + title: group.title, + tag: group.tag, + link: group.link, + labels: tgLabelsByGroupId.get(group.telegramId) ?? [], + group, + })) + + const waRows: CombinedGroupRow[] = loadedWaGroups + .filter((group) => hasExactLabel(waLabelsByGroupId.get(group.id) ?? [], path)) + .map((group) => ({ + key: `whatsapp-${group.id}`, + platform: "whatsapp", + title: group.title, + tag: null, + link: group.link, + labels: waLabelsByGroupId.get(group.id) ?? [], + group, + })) + + return [...tgRows, ...waRows] + }, [loadedTgGroups, loadedWaGroups, tgLabelsByGroupId, waLabelsByGroupId, path]) + + const visibleRows = useMemo(() => { + const normalizedQuery = query.trim().toLocaleLowerCase().replace(/^@/, "") + if (!normalizedQuery) return branchRows + return branchRows.filter((row) => + [row.title, row.tag].filter(Boolean).join(" ").toLocaleLowerCase().includes(normalizedQuery) + ) + }, [branchRows, query]) + + // Guarantees Didattica and Extra always appear, even with zero labels yet — otherwise an empty root would + // have no card to click and no way back into it once it's the only path left to reach it. + const categoryTree = useMemo( + () => buildCategoryRootTree(loadedGroupLabels.filter((label) => isCategoryLabel(label.label))), + [loadedGroupLabels] + ) + // An empty path is the top of the tree (the "Categories" landing page) — its sub-categories are the tree's + // own roots (Didattica, Extra), not a lookup, since there's no node for the empty path itself. + const isRoot = path === "" + const subCategories = useMemo( + () => (isRoot ? categoryTree : (findLabelTreeNode(categoryTree, path)?.children ?? [])), + [categoryTree, path, isRoot] + ) + + const hasSearch = Boolean(query.trim()) + const labelExists = loadedGroupLabels.some((label) => label.label === path) + const segments = labelPathToUrlSegments(path) + const title = isRoot ? "Categories" : formatLabelSegment(segments[segments.length - 1] ?? path) + const eyebrow = segments.length > 1 ? formatLabelBreadcrumb(segments.slice(0, -1).join(".")) : "Web" + + // At a top-level category (Didattica, Extra) "up" goes back to the Categories landing page (the Didattica/Extra + // picker), not to the Group labels management page — browsing and managing are two separate destinations now + // that there's no sidebar tree to jump around the hierarchy with. + const parentSegments = segments.slice(0, -1) + const backUrl: string = parentSegments.length + ? `/dashboard/web/groups-by-label/${parentSegments.join("/")}` + : "/dashboard/web/groups-by-label" + const backLabel = parentSegments.length ? formatLabelSegment(parentSegments[parentSegments.length - 1]) : "Categories" + + return ( +
+ {!isRoot && ( + + )} + + + +
+ ) + } + /> + + {subCategories.length > 0 && ( +
+

{isRoot ? "Categories" : "Sub-categories"}

+
+ {subCategories.map((child) => { + const childUrl: string = `/dashboard/web/groups-by-label/${labelPathToUrlSegments(child.path).join("/")}` + return ( + + {formatLabelSegment(child.segment)} + + + ) + })} +
+
+ )} + + {!isRoot && ( + <> + {subCategories.length > 0 &&

Groups

} + 0 + ? "No groups are tagged with this exact category — check the sub-categories above." + : "No groups are tagged with this category yet." + } + /> + + )} + + ) +} diff --git a/src/features/guides/guides.functions.ts b/src/features/guides/guides.functions.ts index 13b2c80..33a1da2 100644 --- a/src/features/guides/guides.functions.ts +++ b/src/features/guides/guides.functions.ts @@ -1,16 +1,16 @@ import { createServerFn } from "@tanstack/react-start" import { z } from "zod" -import { adminMiddleware, writeAdminMiddleware } from "@/server/auth.middleware" +import { webAdminMiddleware, webWriteAdminMiddleware } from "@/server/auth.middleware" import { parseGuideForm } from "./guides.validation" export const getGuides = createServerFn() - .middleware([adminMiddleware]) + .middleware([webAdminMiddleware]) .handler(({ context }) => context.backend.web.guides_matricole.getAllGuides.query()) export const createGuide = createServerFn({ method: "POST" }) - .middleware([writeAdminMiddleware]) + .middleware([webWriteAdminMiddleware]) .validator(parseGuideForm) .handler(async ({ data, context }) => { const formData = new FormData() @@ -25,7 +25,7 @@ export const createGuide = createServerFn({ method: "POST" }) }) export const deleteGuide = createServerFn({ method: "POST" }) - .middleware([writeAdminMiddleware]) + .middleware([webWriteAdminMiddleware]) .validator(z.object({ id: z.number().int().positive() })) .handler(async ({ data, context }) => { const result = await context.backend.web.guides_matricole.deleteGuide.mutate(data) diff --git a/src/features/projects/projects.functions.ts b/src/features/projects/projects.functions.ts index b3a12b2..f1c2483 100644 --- a/src/features/projects/projects.functions.ts +++ b/src/features/projects/projects.functions.ts @@ -1,7 +1,7 @@ import { createServerFn } from "@tanstack/react-start" import { z } from "zod" -import { adminMiddleware, writeAdminMiddleware } from "@/server/auth.middleware" +import { webAdminMiddleware, webWriteAdminMiddleware } from "@/server/auth.middleware" import { parseProjectForm } from "./projects.validation" import type { Project } from "./types" @@ -30,14 +30,14 @@ function projectFormData(data: ReturnType) { } export const getProjects = createServerFn() - .middleware([adminMiddleware]) + .middleware([webAdminMiddleware]) .handler(async ({ context }) => { const projects = await context.backend.web.projects.getAllProjects.query() return projects.map(projectFields) }) export const createProject = createServerFn({ method: "POST" }) - .middleware([writeAdminMiddleware]) + .middleware([webWriteAdminMiddleware]) .validator(parseProjectForm) .handler(async ({ data, context }) => { const formData = projectFormData(data) @@ -47,7 +47,7 @@ export const createProject = createServerFn({ method: "POST" }) }) export const editProject = createServerFn({ method: "POST" }) - .middleware([writeAdminMiddleware]) + .middleware([webWriteAdminMiddleware]) .validator((data: FormData) => { const id = Number(data.get("id")) if (!Number.isInteger(id) || id <= 0) throw new Error("INVALID_ID") @@ -63,7 +63,7 @@ export const editProject = createServerFn({ method: "POST" }) }) export const deleteProject = createServerFn({ method: "POST" }) - .middleware([writeAdminMiddleware]) + .middleware([webWriteAdminMiddleware]) .validator(z.object({ id: z.number().int().positive() })) .handler(async ({ data, context }) => { const result = await context.backend.web.projects.deleteProject.mutate(data) @@ -72,7 +72,7 @@ export const deleteProject = createServerFn({ method: "POST" }) }) export const reorderProjects = createServerFn({ method: "POST" }) - .middleware([writeAdminMiddleware]) + .middleware([webWriteAdminMiddleware]) .validator(z.object({ projectIds: z.array(z.number().int().positive()).min(1) })) .handler(async ({ data, context }) => { const result = await context.backend.web.projects.reorderProjects.mutate(data) diff --git a/src/features/telegram/grants-page.tsx b/src/features/telegram/grants-page.tsx index 7f44df1..a0cea8f 100644 --- a/src/features/telegram/grants-page.tsx +++ b/src/features/telegram/grants-page.tsx @@ -28,7 +28,13 @@ function formatDate(value: Date | string) { return new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "short" }).format(new Date(value)) } -export function TelegramGrantsPage({ grants: { ongoing, scheduled } }: { grants: TelegramGrants }) { +export function TelegramGrantsPage({ + grants: { ongoing, scheduled }, + canWrite, +}: { + grants: TelegramGrants + canWrite: boolean +}) { const [tab, setTab] = useState<"all" | "ongoing" | "scheduled">("all") const ongoingGrants = ongoing.grants.map((record): GrantRow => ({ ...record, status: "active" })) const scheduledGrants = scheduled.grants.map((record): GrantRow => ({ ...record, status: "scheduled" })) @@ -161,7 +167,7 @@ export function TelegramGrantsPage({ grants: { ongoing, scheduled } }: { grants: count={filteredCount} total={grants.length} searchPlaceholder="Search users, authorizers, or reasons…" - action={} + action={canWrite ? : undefined} onSearch={(value) => { table.setGlobalFilter(value) table.setPageIndex(0) diff --git a/src/features/telegram/groups-page.tsx b/src/features/telegram/groups-page.tsx index d5b3d51..84d7407 100644 --- a/src/features/telegram/groups-page.tsx +++ b/src/features/telegram/groups-page.tsx @@ -1,198 +1,66 @@ -import { useRouter } from "@tanstack/react-router" -import { useServerFn } from "@tanstack/react-start" -import type { Column } from "@tanstack/react-table" -import { - ArrowDown, - ArrowUp, - ChevronsUpDown, - ExternalLink, - Eye, - EyeOff, - LoaderCircle, - MessageCircleMore, -} from "lucide-react" +import { Tag } from "lucide-react" import { useMemo, useState } from "react" -import { toast } from "sonner" import { DataToolbar } from "@/components/data-toolbar" -import { EmptyState } from "@/components/empty-state" -import { Pagination } from "@/components/pagination" -import { Alert, AlertDescription } from "@/components/ui/alert" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" -import { DataTableHead, Table, TableBody, TableCell, TableHeader, TableRow, TableSurface } from "@/components/ui/table" -import { setGroupVisibility } from "@/features/telegram/groups.functions" -import { LeaveGroupDialog } from "@/features/telegram/leave-group-dialog" -import type { TgGroup } from "@/lib/api/types" -import { createAppColumnHelper, type dashboardFeatures, useAppTable } from "@/lib/table" -import { cn } from "@/lib/utils" - -const groupColumnHelper = createAppColumnHelper() +import { + Popover, + PopoverContent, + PopoverDescription, + PopoverHeader, + PopoverTitle, + PopoverTrigger, +} from "@/components/ui/popover" +import { isSameGroupLabel } from "@/features/group-labels/group-labels.constants" +import { buildLabelsByGroupId } from "@/features/group-labels/label-tree" +import { LabelTreeSelector } from "@/features/group-labels/label-tree-selector" +import { GroupsTable } from "@/features/telegram/groups-table" +import type { GroupWithLabels, TgGroup, TgGroupLabel } from "@/lib/api/types" -export function TelegramGroupsPage({ loadedGroups }: { loadedGroups: TgGroup[] }) { - const router = useRouter() - const setGroupVisibilityFn = useServerFn(setGroupVisibility) - const [visibilityOverrides, setVisibilityOverrides] = useState>({}) - const [updatingId, setUpdatingId] = useState(null) - const [mutationError, setMutationError] = useState("") - const [refreshError, setRefreshError] = useState("") +function setManyGroupLabels(current: TgGroupLabel[], labels: TgGroupLabel[], select: boolean): TgGroupLabel[] { + if (select) { + const toAdd = labels.filter((label) => !current.some((existing) => isSameGroupLabel(existing, label))) + return [...current, ...toAdd] + } + return current.filter((existing) => !labels.some((label) => isSameGroupLabel(existing, label))) +} - const groups = loadedGroups.map((group) => ({ - ...group, - hide: visibilityOverrides[group.telegramId] ?? group.hide, - })) +export function TelegramGroupsPage({ + loadedGroups, + loadedGroupLabels, + loadedGroupsWithLabels, +}: { + loadedGroups: TgGroup[] + loadedGroupLabels: TgGroupLabel[] + loadedGroupsWithLabels: GroupWithLabels[] +}) { + const [query, setQuery] = useState("") + const [requiredLabels, setRequiredLabels] = useState([]) + const [excludedLabels, setExcludedLabels] = useState([]) - async function toggleVisibility(group: TgGroup) { - if (updatingId !== null) return - const hide = !group.hide - setUpdatingId(group.telegramId) - setMutationError("") - setVisibilityOverrides((current) => ({ ...current, [group.telegramId]: hide })) + const labelsByGroupId = useMemo( + () => buildLabelsByGroupId(loadedGroupLabels, loadedGroupsWithLabels, "tg"), + [loadedGroupLabels, loadedGroupsWithLabels] + ) - try { - await setGroupVisibilityFn({ data: { telegramId: group.telegramId, hide } }) - toast.success(`${group.title} is now ${hide ? "hidden" : "visible"}.`) - try { - await router.invalidate({ sync: true }) - setRefreshError("") - setVisibilityOverrides((current) => { - const { [group.telegramId]: _removed, ...remaining } = current - return remaining - }) - } catch (error) { - console.error(error) - setRefreshError("The visibility was updated, but the latest group data could not be refreshed.") - } - } catch (error) { - console.error(error) - setVisibilityOverrides((current) => { - const { [group.telegramId]: _removed, ...remaining } = current - return remaining - }) - setMutationError("The visibility setting could not be updated. Check your permissions and try again.") - } finally { - setUpdatingId(null) - } - } + const visibleGroups = useMemo(() => { + const normalizedQuery = query.trim().toLocaleLowerCase().replace(/^@/, "") + return loadedGroups.filter((group) => { + const matchesText = + !normalizedQuery || + [group.title, group.tag].filter(Boolean).join(" ").toLocaleLowerCase().includes(normalizedQuery) + if (!matchesText) return false - const columns = useMemo(() => { - const sortableHeader = ( - label: string, - column: Pick, "getIsSorted" | "getToggleSortingHandler"> - ) => { - const sorted = column.getIsSorted() - const Icon = !sorted ? ChevronsUpDown : sorted === "asc" ? ArrowUp : ArrowDown - return ( - - ) - } + const groupLabels = labelsByGroupId.get(group.telegramId) ?? [] + const matchesRequired = requiredLabels.every((label) => groupLabels.some((gl) => gl.label === label.label)) + const matchesExcluded = excludedLabels.every((label) => !groupLabels.some((gl) => gl.label === label.label)) + return matchesRequired && matchesExcluded + }) + }, [loadedGroups, query, requiredLabels, excludedLabels, labelsByGroupId]) - return groupColumnHelper.columns([ - groupColumnHelper.accessor("title", { - header: ({ column }) => sortableHeader("Group", column), - cell: ({ row }) => ( -
- - - - {row.original.title} -
- ), - }), - groupColumnHelper.accessor("telegramId", { - header: ({ column }) => sortableHeader("Telegram ID", column), - cell: ({ getValue }) => {getValue()}, - }), - groupColumnHelper.accessor("tag", { - header: ({ column }) => sortableHeader("Tag", column), - cell: ({ getValue }) => - getValue() ? ( - - @{getValue()} - - ) : ( - Not set - ), - }), - groupColumnHelper.display({ - id: "visibility", - header: "Visibility", - cell: ({ row }) => { - const group = row.original - const pending = updatingId === group.telegramId - const visible = !group.hide - return ( - - ) - }, - }), - groupColumnHelper.display({ - id: "invite", - header: "Invite", - cell: ({ row }) => { - const link = row.original.link - return link ? ( - - - Open invite link - - ) : ( - Not shared - ) - }, - }), - groupColumnHelper.display({ - id: "leave", - header: "", - cell: ({ row }) => , - }), - ]) - }, [updatingId]) - const table = useAppTable({ - key: "telegram-groups", - columns, - data: groups, - initialState: { sorting: [{ id: "title", desc: false }], pagination: { pageIndex: 0, pageSize: 20 } }, - globalFilterFn: (row, _columnId, value) => { - const group = row.original - const query = String(value ?? "") - .trim() - .toLocaleLowerCase() - .replace(/^@/, "") - return !query || [group.title, group.tag].filter(Boolean).join(" ").toLocaleLowerCase().includes(query) - }, - }) - const filteredCount = table.getFilteredRowModel().rows.length - const hasSearch = Boolean(String(table.state.globalFilter ?? "").trim()) + const activeLabelFilterCount = requiredLabels.length + excludedLabels.length + const hasFilters = Boolean(query.trim()) || activeLabelFilterCount > 0 return (
@@ -200,79 +68,83 @@ export function TelegramGroupsPage({ loadedGroups }: { loadedGroups: TgGroup[] } eyebrow="Telegram" title="Telegram groups" description="Maintain the community groups connected to PoliNetwork." - count={filteredCount} - total={groups.length} + count={visibleGroups.length} + total={loadedGroups.length} searchPlaceholder="Search by group name or tag…" - onSearch={(value) => { - table.setGlobalFilter(value) - table.setPageIndex(0) - }} - /> - {mutationError && ( - - {mutationError} - - )} - {refreshError && ( - - {refreshError} - - )} - {filteredCount ? ( - <> - - - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - {header.isPlaceholder ? null : } - - ))} - - ))} - - - {table.getRowModel().rows.map((row) => ( - - {row.getAllCells().map((cell) => ( - - - - ))} - - ))} - -
-
- table.setPageIndex(page - 1)} - onPageSizeChange={(pageSize) => table.setPageSize(pageSize)} + onSearch={setQuery} + > + + + + Labels + {activeLabelFilterCount > 0 && ( + + {activeLabelFilterCount} + + )} + + } /> - - ) : ( - - )} + + + Filter by label + Show groups that must, or must not, have specific labels. + + {loadedGroupLabels.length ? ( + <> +
+

Must have

+ !excludedLabels.some((excluded) => isSameGroupLabel(excluded, label)) + )} + selected={requiredLabels} + onToggleMany={(labels, select) => + setRequiredLabels((current) => setManyGroupLabels(current, labels, select)) + } + /> +
+
+

Must not have

+ !requiredLabels.some((required) => isSameGroupLabel(required, label)) + )} + selected={excludedLabels} + onToggleMany={(labels, select) => + setExcludedLabels((current) => setManyGroupLabels(current, labels, select)) + } + /> +
+ {activeLabelFilterCount > 0 && ( + + )} + + ) : ( +

No labels have been created yet.

+ )} +
+
+ +
) } diff --git a/src/features/telegram/groups-table.tsx b/src/features/telegram/groups-table.tsx new file mode 100644 index 0000000..4e0b126 --- /dev/null +++ b/src/features/telegram/groups-table.tsx @@ -0,0 +1,310 @@ +import { useRouter } from "@tanstack/react-router" +import { useServerFn } from "@tanstack/react-start" +import type { Column } from "@tanstack/react-table" +import { + ArrowDown, + ArrowUp, + ChevronsUpDown, + ExternalLink, + Eye, + EyeOff, + LoaderCircle, + type LucideIcon, + MessageCircleMore, +} from "lucide-react" +import { useMemo, useState } from "react" +import { toast } from "sonner" + +import { EmptyState } from "@/components/empty-state" +import { Pagination } from "@/components/pagination" +import { Alert, AlertDescription } from "@/components/ui/alert" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { DataTableHead, Table, TableBody, TableCell, TableHeader, TableRow, TableSurface } from "@/components/ui/table" +import { GroupLabelBadges } from "@/features/group-labels/group-label-badges" +import { GroupLabelsDialog } from "@/features/group-labels/group-labels-dialog" +import { setGroupVisibility } from "@/features/telegram/groups.functions" +import { LeaveGroupDialog } from "@/features/telegram/leave-group-dialog" +import type { TgGroup, TgGroupLabel } from "@/lib/api/types" +import { createAppColumnHelper, type dashboardFeatures, useAppTable } from "@/lib/table" +import { cn } from "@/lib/utils" + +const groupColumnHelper = createAppColumnHelper() + +/** The interactive groups table (visibility toggle, invite link, leave, click-to-edit-labels), given an already-filtered group list. */ +export function GroupsTable({ + groups: loadedGroups, + allLabels, + labelsByGroupId, + emptyIcon = MessageCircleMore, + emptyTitle, + emptyText, +}: { + groups: TgGroup[] + allLabels: TgGroupLabel[] + labelsByGroupId: Map + emptyIcon?: LucideIcon + emptyTitle: string + emptyText: string +}) { + const router = useRouter() + const setGroupVisibilityFn = useServerFn(setGroupVisibility) + const [visibilityOverrides, setVisibilityOverrides] = useState>({}) + const [updatingId, setUpdatingId] = useState(null) + const [mutationError, setMutationError] = useState("") + const [refreshError, setRefreshError] = useState("") + const [editingGroup, setEditingGroup] = useState(null) + + const groups = loadedGroups.map((group) => ({ + ...group, + hide: visibilityOverrides[group.telegramId] ?? group.hide, + })) + + async function toggleVisibility(group: TgGroup) { + if (updatingId !== null) return + const hide = !group.hide + setUpdatingId(group.telegramId) + setMutationError("") + setVisibilityOverrides((current) => ({ ...current, [group.telegramId]: hide })) + + try { + await setGroupVisibilityFn({ data: { telegramId: group.telegramId, hide } }) + toast.success(`${group.title} is now ${hide ? "hidden" : "visible"}.`) + try { + await router.invalidate({ sync: true }) + setRefreshError("") + setVisibilityOverrides((current) => { + const { [group.telegramId]: _removed, ...remaining } = current + return remaining + }) + } catch (error) { + console.error(error) + setRefreshError("The visibility was updated, but the latest group data could not be refreshed.") + } + } catch (error) { + console.error(error) + setVisibilityOverrides((current) => { + const { [group.telegramId]: _removed, ...remaining } = current + return remaining + }) + setMutationError("The visibility setting could not be updated. Check your permissions and try again.") + } finally { + setUpdatingId(null) + } + } + + const columns = useMemo(() => { + const sortableHeader = ( + label: string, + column: Pick, "getIsSorted" | "getToggleSortingHandler"> + ) => { + const sorted = column.getIsSorted() + const Icon = !sorted ? ChevronsUpDown : sorted === "asc" ? ArrowUp : ArrowDown + return ( + + ) + } + + return groupColumnHelper.columns([ + groupColumnHelper.accessor("title", { + header: ({ column }) => sortableHeader("Group", column), + cell: ({ row }) => ( +
+ + + + {row.original.title} +
+ ), + }), + groupColumnHelper.accessor("telegramId", { + header: ({ column }) => sortableHeader("Telegram ID", column), + cell: ({ getValue }) => {getValue()}, + }), + groupColumnHelper.accessor("tag", { + header: ({ column }) => sortableHeader("Tag", column), + cell: ({ getValue }) => + getValue() ? ( + + @{getValue()} + + ) : ( + Not set + ), + }), + groupColumnHelper.display({ + id: "labels", + header: "Labels", + cell: ({ row }) => , + }), + groupColumnHelper.display({ + id: "visibility", + header: "Visibility", + cell: ({ row }) => { + const group = row.original + const pending = updatingId === group.telegramId + const visible = !group.hide + return ( +
event.stopPropagation()}> + +
+ ) + }, + }), + groupColumnHelper.display({ + id: "invite", + header: "Invite", + cell: ({ row }) => { + const link = row.original.link + return link ? ( + event.stopPropagation()} + > + + Open invite link + + ) : ( + Not shared + ) + }, + }), + groupColumnHelper.display({ + id: "leave", + header: "", + cell: ({ row }) => ( +
event.stopPropagation()}> + +
+ ), + }), + ]) + }, [updatingId, labelsByGroupId]) + + const table = useAppTable({ + key: "telegram-groups", + columns, + data: groups, + initialState: { + sorting: [{ id: "title", desc: false }], + pagination: { pageIndex: 0, pageSize: 20 }, + }, + }) + + return ( + <> + {mutationError && ( + + {mutationError} + + )} + {refreshError && ( + + {refreshError} + + )} + {groups.length ? ( + <> + + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder ? null : } + + ))} + + ))} + + + {table.getRowModel().rows.map((row) => ( + setEditingGroup(row.original)} + onKeyDown={(event) => { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault() + setEditingGroup(row.original) + } + }} + > + {row.getAllCells().map((cell) => ( + + + + ))} + + ))} + +
+
+ table.setPageIndex(page - 1)} + onPageSizeChange={(pageSize) => table.setPageSize(pageSize)} + /> + + ) : ( + + )} + setEditingGroup(null)} + onSaved={async () => { + try { + await router.invalidate({ sync: true }) + } catch (error) { + console.error(error) + toast.warning("The labels were saved, but the group list could not be refreshed.") + } + }} + /> + + ) +} diff --git a/src/features/telegram/groups.functions.ts b/src/features/telegram/groups.functions.ts index c32a6b6..6dc9c09 100644 --- a/src/features/telegram/groups.functions.ts +++ b/src/features/telegram/groups.functions.ts @@ -1,14 +1,14 @@ import { createServerFn } from "@tanstack/react-start" import { z } from "zod" -import { adminMiddleware, writeAdminMiddleware } from "@/server/auth.middleware" +import { adminMiddleware, groupWriteAdminMiddleware } from "@/server/auth.middleware" export const getTelegramGroups = createServerFn() .middleware([adminMiddleware]) .handler(({ context }) => context.backend.tg.groups.getAll.query()) export const setGroupVisibility = createServerFn({ method: "POST" }) - .middleware([writeAdminMiddleware]) + .middleware([groupWriteAdminMiddleware]) .validator(z.object({ telegramId: z.number().int(), hide: z.boolean() })) .handler(async ({ data, context }) => { const updated = await context.backend.tg.groups.setHide.mutate(data) @@ -17,7 +17,7 @@ export const setGroupVisibility = createServerFn({ method: "POST" }) }) export const leaveTelegramGroup = createServerFn({ method: "POST" }) - .middleware([writeAdminMiddleware]) + .middleware([groupWriteAdminMiddleware]) .validator(z.object({ chatId: z.number().int() })) .handler(({ data, context }) => context.backend.tg.groups.leaveChat.mutate({ chatId: data.chatId, performerId: context.telegramId }) diff --git a/src/features/telegram/user-detail/profile.tsx b/src/features/telegram/user-detail/profile.tsx index fd96472..efad359 100644 --- a/src/features/telegram/user-detail/profile.tsx +++ b/src/features/telegram/user-detail/profile.tsx @@ -59,7 +59,7 @@ function GrantDetails({ ) } -export function TelegramUserProfile({ data }: { data: TelegramUserDetail }) { +export function TelegramUserProfile({ data, canWrite }: { data: TelegramUserDetail; canWrite: boolean }) { const router = useRouter() const [adminDialogOpen, setAdminDialogOpen] = useState(false) const { user, roles, configuredRoles, groupAdmin, groups, messages, audits, ongoingGrant, scheduledGrants } = data @@ -104,10 +104,12 @@ export function TelegramUserProfile({ data }: { data: TelegramUserDetail }) { icon={ShieldCheck} label="ROLES" actions={ - <> - - - + canWrite ? ( + <> + + + + ) : undefined } >
@@ -126,10 +128,12 @@ export function TelegramUserProfile({ data }: { data: TelegramUserDetail }) { icon={CalendarClock} label="GRANTS" actions={ - <> - - {ongoingGrant && } - + canWrite ? ( + <> + + {ongoingGrant && } + + ) : undefined } > {ongoingGrant || scheduledGrants.length ? ( @@ -163,9 +167,11 @@ export function TelegramUserProfile({ data }: { data: TelegramUserDetail }) { title="Group administration" count={administeredGroups.length} action={ - + canWrite ? ( + + ) : undefined } >
@@ -178,45 +184,49 @@ export function TelegramUserProfile({ data }: { data: TelegramUserDetail }) { Added by {entry.addedBy.firstName} {entry.addedBy.username ? ` · @${entry.addedBy.username}` : ""} -
- { - toast.success("Group administrator removed.") - try { - await router.invalidate({ sync: true }) - } catch (error) { - console.error(error) - toast.warning("The assignment was removed, but the latest user data could not be refreshed.") - } - }} - /> -
+ {canWrite && ( +
+ { + toast.success("Group administrator removed.") + try { + await router.invalidate({ sync: true }) + } catch (error) { + console.error(error) + toast.warning("The assignment was removed, but the latest user data could not be refreshed.") + } + }} + /> +
+ )} ))} {!administeredGroups.length && }
- entry.group.id))} - onClose={() => setAdminDialogOpen(false)} - onSaved={async () => { - setAdminDialogOpen(false) - toast.success("Group administrator added.") - try { - await router.invalidate({ sync: true }) - } catch (error) { - console.error(error) - toast.warning("The administrator was added, but the latest user data could not be refreshed.") - } - }} - /> + {canWrite && ( + entry.group.id))} + onClose={() => setAdminDialogOpen(false)} + onSaved={async () => { + setAdminDialogOpen(false) + toast.success("Group administrator added.") + try { + await router.invalidate({ sync: true }) + } catch (error) { + console.error(error) + toast.warning("The administrator was added, but the latest user data could not be refreshed.") + } + }} + /> + )}
{messages.map((message) => ( diff --git a/src/features/whatsapp/create-edit-group-dialog.tsx b/src/features/whatsapp/create-edit-group-dialog.tsx new file mode 100644 index 0000000..fd137d0 --- /dev/null +++ b/src/features/whatsapp/create-edit-group-dialog.tsx @@ -0,0 +1,153 @@ +import { useRouter } from "@tanstack/react-router" +import { useServerFn } from "@tanstack/react-start" +import { LoaderCircle, Pencil, Plus } from "lucide-react" +import { useState } from "react" +import { toast } from "sonner" + +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { DEFAULT_GROUP_LABEL_COLOR } from "@/features/group-labels/group-labels.constants" +import { createGroupLabel, tagGroup } from "@/features/group-labels/group-labels.functions" +import { createWhatsappGroup, editWhatsappGroup } from "@/features/whatsapp/groups.functions" +import { WhatsappGroupFields } from "@/features/whatsapp/whatsapp-group-fields" +import { isValidWhatsappInviteLink } from "@/features/whatsapp/whatsapp.validation" +import type { WaGroup } from "@/lib/api/types" +import { errorMessage } from "@/lib/errors" + +export function CreateEditGroupDialog({ + group, + autoAssignLabel, + autoAssignLabelExists = false, +}: { + group?: WaGroup + /** When set (e.g. from a "groups by label" branch page), the new group is tagged with this label after creation. */ + autoAssignLabel?: string + /** Whether `autoAssignLabel` already exists as a real label; if not, it's created first. */ + autoAssignLabelExists?: boolean +}) { + const router = useRouter() + const createGroupFn = useServerFn(createWhatsappGroup) + const editGroupFn = useServerFn(editWhatsappGroup) + const createGroupLabelFn = useServerFn(createGroupLabel) + const tagGroupFn = useServerFn(tagGroup) + const [open, setOpen] = useState(false) + const [title, setTitle] = useState(group?.title ?? "") + const [link, setLink] = useState(group?.link ?? "") + const [pending, setPending] = useState(false) + const [error, setError] = useState("") + + const canSave = Boolean(title.trim()) && isValidWhatsappInviteLink(link.trim()) + + function reset() { + setTitle(group?.title ?? "") + setLink(group?.link ?? "") + setError("") + } + + async function submit(event: React.FormEvent) { + event.preventDefault() + if (!canSave || pending) return + setPending(true) + setError("") + try { + const values = { title: title.trim(), link: link.trim() } + if (group) { + await editGroupFn({ data: { id: group.id, ...values } }) + toast.success(`${values.title} updated.`) + } else { + const created = await createGroupFn({ data: values }) + if (autoAssignLabel) { + try { + if (!autoAssignLabelExists) { + await createGroupLabelFn({ + data: { label: autoAssignLabel, color: DEFAULT_GROUP_LABEL_COLOR, description: "" }, + }) + } + await tagGroupFn({ data: { groupId: created.id, type: "wa", label: autoAssignLabel } }) + toast.success(`${values.title} added and labeled "${autoAssignLabel}".`) + } catch (tagCause) { + console.error(tagCause) + toast.warning( + `${values.title} was added, but could not be labeled "${autoAssignLabel}". Assign it manually.` + ) + } + } else { + toast.success(`${values.title} added.`) + } + } + setOpen(false) + if (!group) reset() + await router.invalidate({ sync: true }) + } catch (cause) { + console.error(cause) + setError(errorMessage(cause, "The group could not be saved. Check the details and try again.")) + } finally { + setPending(false) + } + } + + return ( + { + if (pending) return + setOpen(nextOpen) + if (!nextOpen) reset() + }} + > + : + + + + + + ) +} diff --git a/src/features/whatsapp/delete-group-dialog.tsx b/src/features/whatsapp/delete-group-dialog.tsx new file mode 100644 index 0000000..392595c --- /dev/null +++ b/src/features/whatsapp/delete-group-dialog.tsx @@ -0,0 +1,74 @@ +import { useRouter } from "@tanstack/react-router" +import { useServerFn } from "@tanstack/react-start" +import { LoaderCircle, Trash2 } from "lucide-react" +import { useState } from "react" +import { toast } from "sonner" + +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog" +import { Button } from "@/components/ui/button" +import { deleteWhatsappGroup } from "@/features/whatsapp/groups.functions" +import { errorMessage } from "@/lib/errors" + +export function DeleteGroupDialog({ id, title }: { id: number; title: string }) { + const router = useRouter() + const deleteGroupFn = useServerFn(deleteWhatsappGroup) + const [open, setOpen] = useState(false) + const [pending, setPending] = useState(false) + const [error, setError] = useState("") + + async function remove() { + setPending(true) + setError("") + try { + await deleteGroupFn({ data: { id } }) + toast.success(`${title} deleted.`) + setOpen(false) + await router.invalidate({ sync: true }) + } catch (cause) { + console.error(cause) + setError(errorMessage(cause, "The group could not be deleted. Check your permissions and try again.")) + } finally { + setPending(false) + } + } + + return ( + { + if (pending) return + setOpen(nextOpen) + if (!nextOpen) setError("") + }} + > + }> + + Delete {title} + + + + Delete {title}? + This removes the group record. This action cannot be undone. + + {error &&

{error}

} + + Cancel + void remove()}> + {pending && } + Delete + + +
+
+ ) +} diff --git a/src/features/whatsapp/groups.functions.ts b/src/features/whatsapp/groups.functions.ts new file mode 100644 index 0000000..76c88a8 --- /dev/null +++ b/src/features/whatsapp/groups.functions.ts @@ -0,0 +1,38 @@ +import { createServerFn } from "@tanstack/react-start" +import { z } from "zod" + +import { adminMiddleware, groupWriteAdminMiddleware } from "@/server/auth.middleware" + +import { whatsappInviteLink } from "./whatsapp.validation" + +export const getWhatsappGroups = createServerFn() + .middleware([adminMiddleware]) + .handler(({ context }) => context.backend.wa.groups.getAll.query()) + +const whatsappGroupFields = z.object({ + title: z.string().trim().min(1).max(200), + link: whatsappInviteLink, +}) + +export const createWhatsappGroup = createServerFn({ method: "POST" }) + .middleware([groupWriteAdminMiddleware]) + .validator(whatsappGroupFields) + .handler(({ data, context }) => context.backend.wa.groups.add.mutate(data)) + +export const editWhatsappGroup = createServerFn({ method: "POST" }) + .middleware([groupWriteAdminMiddleware]) + .validator(whatsappGroupFields.extend({ id: z.number() })) + .handler(async ({ data, context }) => { + const updated = await context.backend.wa.groups.modify.mutate(data) + if (!updated) throw new Error("NOT_FOUND") + return updated + }) + +export const deleteWhatsappGroup = createServerFn({ method: "POST" }) + .middleware([groupWriteAdminMiddleware]) + .validator(z.object({ id: z.number() })) + .handler(async ({ data, context }) => { + const deleted = await context.backend.wa.groups.delete.mutate({ id: data.id }) + if (!deleted) throw new Error("NOT_FOUND") + return { error: null } + }) diff --git a/src/features/whatsapp/whatsapp-group-fields.tsx b/src/features/whatsapp/whatsapp-group-fields.tsx new file mode 100644 index 0000000..5eaba88 --- /dev/null +++ b/src/features/whatsapp/whatsapp-group-fields.tsx @@ -0,0 +1,50 @@ +import { useId } from "react" + +import { Field, FieldLabel } from "@/components/ui/field" +import { Input } from "@/components/ui/input" + +import { WHATSAPP_INVITE_LINK_MAX } from "./whatsapp.validation" + +export function WhatsappGroupFields({ + title, + onTitleChange, + link, + onLinkChange, +}: { + title: string + onTitleChange: (value: string) => void + link: string + onLinkChange: (value: string) => void +}) { + const titleId = useId() + const linkId = useId() + + return ( + <> + + Title + onTitleChange(event.target.value)} + placeholder="Gruppo Informatica 1" + maxLength={200} + required + autoFocus + /> + + + Invite link + onLinkChange(event.target.value)} + placeholder="https://chat.whatsapp.com/…" + type="url" + maxLength={WHATSAPP_INVITE_LINK_MAX} + required + /> + + + ) +} diff --git a/src/features/whatsapp/whatsapp-groups-page.tsx b/src/features/whatsapp/whatsapp-groups-page.tsx new file mode 100644 index 0000000..55a51b8 --- /dev/null +++ b/src/features/whatsapp/whatsapp-groups-page.tsx @@ -0,0 +1,316 @@ +import { useRouter } from "@tanstack/react-router" +import type { Column } from "@tanstack/react-table" +import { ArrowDown, ArrowUp, ChevronsUpDown, ExternalLink, MessageCircleMore, Tag } from "lucide-react" +import { useMemo, useState } from "react" +import { toast } from "sonner" + +import { DataToolbar } from "@/components/data-toolbar" +import { EmptyState } from "@/components/empty-state" +import { Pagination } from "@/components/pagination" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { + Popover, + PopoverContent, + PopoverDescription, + PopoverHeader, + PopoverTitle, + PopoverTrigger, +} from "@/components/ui/popover" +import { DataTableHead, Table, TableBody, TableCell, TableHeader, TableRow, TableSurface } from "@/components/ui/table" +import { GroupLabelBadges } from "@/features/group-labels/group-label-badges" +import { GroupLabelsDialog } from "@/features/group-labels/group-labels-dialog" +import { isSameGroupLabel } from "@/features/group-labels/group-labels.constants" +import { buildLabelsByGroupId } from "@/features/group-labels/label-tree" +import { LabelTreeSelector } from "@/features/group-labels/label-tree-selector" +import { CreateEditGroupDialog } from "@/features/whatsapp/create-edit-group-dialog" +import { DeleteGroupDialog } from "@/features/whatsapp/delete-group-dialog" +import type { GroupWithLabels, TgGroupLabel, WaGroup } from "@/lib/api/types" +import { createAppColumnHelper, type dashboardFeatures, useAppTable } from "@/lib/table" + +function setManyGroupLabels(current: TgGroupLabel[], labels: TgGroupLabel[], select: boolean): TgGroupLabel[] { + if (select) { + const toAdd = labels.filter((label) => !current.some((existing) => isSameGroupLabel(existing, label))) + return [...current, ...toAdd] + } + return current.filter((existing) => !labels.some((label) => isSameGroupLabel(existing, label))) +} + +const groupColumnHelper = createAppColumnHelper() + +export function WhatsappGroupsPage({ + loadedGroups, + loadedGroupLabels, + loadedGroupsWithLabels, +}: { + loadedGroups: WaGroup[] + loadedGroupLabels: TgGroupLabel[] + loadedGroupsWithLabels: GroupWithLabels[] +}) { + const router = useRouter() + const [query, setQuery] = useState("") + const [requiredLabels, setRequiredLabels] = useState([]) + const [excludedLabels, setExcludedLabels] = useState([]) + const [editingLabelsGroup, setEditingLabelsGroup] = useState(null) + + const labelsByGroupId = useMemo( + () => buildLabelsByGroupId(loadedGroupLabels, loadedGroupsWithLabels, "wa"), + [loadedGroupLabels, loadedGroupsWithLabels] + ) + + const visibleGroups = useMemo(() => { + const normalizedQuery = query.trim().toLocaleLowerCase() + return loadedGroups.filter((group) => { + const matchesText = !normalizedQuery || group.title.toLocaleLowerCase().includes(normalizedQuery) + if (!matchesText) return false + + const groupLabels = labelsByGroupId.get(group.id) ?? [] + const matchesRequired = requiredLabels.every((label) => groupLabels.some((gl) => gl.label === label.label)) + const matchesExcluded = excludedLabels.every((label) => !groupLabels.some((gl) => gl.label === label.label)) + return matchesRequired && matchesExcluded + }) + }, [loadedGroups, query, requiredLabels, excludedLabels, labelsByGroupId]) + + const activeLabelFilterCount = requiredLabels.length + excludedLabels.length + const hasFilters = Boolean(query.trim()) || activeLabelFilterCount > 0 + + const columns = useMemo(() => { + const sortableHeader = ( + label: string, + column: Pick, "getIsSorted" | "getToggleSortingHandler"> + ) => { + const sorted = column.getIsSorted() + const Icon = !sorted ? ChevronsUpDown : sorted === "asc" ? ArrowUp : ArrowDown + return ( + + ) + } + + return groupColumnHelper.columns([ + groupColumnHelper.accessor("title", { + header: ({ column }) => sortableHeader("Group", column), + cell: ({ row }) => ( +
+ + + + {row.original.title} +
+ ), + }), + groupColumnHelper.display({ + id: "labels", + header: "Labels", + cell: ({ row }) => , + }), + groupColumnHelper.display({ + id: "invite", + header: "Invite", + cell: ({ row }) => { + const link = row.original.link + return link ? ( + event.stopPropagation()} + > + + Open invite link + + ) : ( + Not shared + ) + }, + }), + groupColumnHelper.display({ + id: "actions", + header: "", + cell: ({ row }) => ( +
event.stopPropagation()} className="flex items-center gap-1.5"> + + +
+ ), + }), + ]) + }, [labelsByGroupId]) + + const table = useAppTable({ + key: "whatsapp-groups", + columns, + data: visibleGroups, + initialState: { sorting: [{ id: "title", desc: false }], pagination: { pageIndex: 0, pageSize: 20 } }, + }) + + return ( +
+ } + > + + + + Labels + {activeLabelFilterCount > 0 && ( + + {activeLabelFilterCount} + + )} + + } + /> + + + Filter by label + Show groups that must, or must not, have specific labels. + + {loadedGroupLabels.length ? ( + <> +
+

Must have

+ !excludedLabels.some((excluded) => isSameGroupLabel(excluded, label)) + )} + selected={requiredLabels} + onToggleMany={(labels, select) => + setRequiredLabels((current) => setManyGroupLabels(current, labels, select)) + } + /> +
+
+

Must not have

+ !requiredLabels.some((required) => isSameGroupLabel(required, label)) + )} + selected={excludedLabels} + onToggleMany={(labels, select) => + setExcludedLabels((current) => setManyGroupLabels(current, labels, select)) + } + /> +
+ {activeLabelFilterCount > 0 && ( + + )} + + ) : ( +

No labels have been created yet.

+ )} +
+
+
+ {visibleGroups.length ? ( + <> + + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder ? null : } + + ))} + + ))} + + + {table.getRowModel().rows.map((row) => ( + setEditingLabelsGroup(row.original)} + onKeyDown={(event) => { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault() + setEditingLabelsGroup(row.original) + } + }} + > + {row.getAllCells().map((cell) => ( + + + + ))} + + ))} + +
+
+ table.setPageIndex(page - 1)} + onPageSizeChange={(pageSize) => table.setPageSize(pageSize)} + /> + + ) : ( + + )} + setEditingLabelsGroup(null)} + onSaved={async () => { + try { + await router.invalidate({ sync: true }) + } catch (error) { + console.error(error) + toast.warning("The labels were saved, but the group list could not be refreshed.") + } + }} + /> +
+ ) +} diff --git a/src/features/whatsapp/whatsapp.validation.ts b/src/features/whatsapp/whatsapp.validation.ts new file mode 100644 index 0000000..bda58bb --- /dev/null +++ b/src/features/whatsapp/whatsapp.validation.ts @@ -0,0 +1,24 @@ +import { z } from "zod" + +export const WHATSAPP_INVITE_LINK_MAX = 128 + +/** Accepts only HTTPS WhatsApp group invite URLs with a non-empty invite code. */ +export function isValidWhatsappInviteLink(value: string): boolean { + if (value.length > WHATSAPP_INVITE_LINK_MAX || !URL.canParse(value)) return false + + const url = new URL(value) + return ( + url.protocol === "https:" && + url.hostname === "chat.whatsapp.com" && + !url.username && + !url.password && + !url.hash && + /^\/[A-Za-z0-9_-]+\/?$/.test(url.pathname) + ) +} + +export const whatsappInviteLink = z + .string() + .trim() + .max(WHATSAPP_INVITE_LINK_MAX) + .refine(isValidWhatsappInviteLink, "Enter a valid WhatsApp group invite link.") diff --git a/src/lib/api/types.ts b/src/lib/api/types.ts index 5769fc2..1c3c8c9 100644 --- a/src/lib/api/types.ts +++ b/src/lib/api/types.ts @@ -8,6 +8,7 @@ export type ApiError = inferRouterError export type TgUser = NonNullable export type TgGrant = NonNullable export type TgGroup = ApiOutput["tg"]["groups"]["getAll"][number] +export type TgGroupLabel = ApiOutput["tg"]["groupLabels"]["getAll"][number] export type TgUserRole = NonNullable export type AzureMember = ApiOutput["azure"]["members"]["getAll"][number] @@ -16,3 +17,8 @@ export type AzureGroup = ApiOutput["azure"]["groups"]["getAll"][number] export type FAQs = ApiOutput["web"]["faqs"]["getAllFaqs"] export type FAQItem = FAQs[number]["faqs"][number] export type WebProject = ApiOutput["web"]["projects"]["getAllProjects"][number] + +export type WaGroup = ApiOutput["wa"]["groups"]["getAll"][number] + +/** A group (Telegram or WhatsApp) with its labels already resolved, from the cross-platform search router. */ +export type GroupWithLabels = ApiOutput["groups"]["search"]["getAll"][number] diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 510c331..dde2f3f 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -15,9 +15,12 @@ import { Route as IndexRouteImport } from './routes/index' import { Route as DashboardIndexRouteImport } from './routes/dashboard/index' import { Route as OnboardingUnauthorizedRouteImport } from './routes/onboarding/unauthorized' import { Route as OnboardingLinkRouteImport } from './routes/onboarding/link' +import { Route as DashboardWebRouteImport } from './routes/dashboard/web' import { Route as DashboardAccountRouteImport } from './routes/dashboard/account' +import { Route as DashboardWhatsappGroupsRouteImport } from './routes/dashboard/whatsapp/groups' import { Route as DashboardWebProjectsRouteImport } from './routes/dashboard/web/projects' import { Route as DashboardWebGuidesRouteImport } from './routes/dashboard/web/guides' +import { Route as DashboardWebGroupLabelsRouteImport } from './routes/dashboard/web/group-labels' import { Route as DashboardWebFaqsRouteImport } from './routes/dashboard/web/faqs' import { Route as DashboardWebAssociationsRouteImport } from './routes/dashboard/web/associations' import { Route as DashboardTelegramGroupsRouteImport } from './routes/dashboard/telegram/groups' @@ -25,7 +28,9 @@ import { Route as DashboardTelegramGrantsRouteImport } from './routes/dashboard/ import { Route as DashboardAzureMembersRouteImport } from './routes/dashboard/azure/members' import { Route as DashboardAzureGroupsRouteImport } from './routes/dashboard/azure/groups' import { Route as ApiAuthSplatRouteImport } from './routes/api/auth/$' +import { Route as DashboardWebGroupsByLabelIndexRouteImport } from './routes/dashboard/web/groups-by-label/index' import { Route as DashboardTelegramUsersIndexRouteImport } from './routes/dashboard/telegram/users/index' +import { Route as DashboardWebGroupsByLabelSplatRouteImport } from './routes/dashboard/web/groups-by-label/$' import { Route as DashboardTelegramUsersUserIdRouteImport } from './routes/dashboard/telegram/users/$userId' const LoginRoute = LoginRouteImport.update({ @@ -58,31 +63,46 @@ const OnboardingLinkRoute = OnboardingLinkRouteImport.update({ path: '/onboarding/link', getParentRoute: () => rootRouteImport, } as any) +const DashboardWebRoute = DashboardWebRouteImport.update({ + id: '/web', + path: '/web', + getParentRoute: () => DashboardRoute, +} as any) const DashboardAccountRoute = DashboardAccountRouteImport.update({ id: '/account', path: '/account', getParentRoute: () => DashboardRoute, } as any) -const DashboardWebProjectsRoute = DashboardWebProjectsRouteImport.update({ - id: '/web/projects', - path: '/web/projects', +const DashboardWhatsappGroupsRoute = DashboardWhatsappGroupsRouteImport.update({ + id: '/whatsapp/groups', + path: '/whatsapp/groups', getParentRoute: () => DashboardRoute, } as any) +const DashboardWebProjectsRoute = DashboardWebProjectsRouteImport.update({ + id: '/projects', + path: '/projects', + getParentRoute: () => DashboardWebRoute, +} as any) const DashboardWebGuidesRoute = DashboardWebGuidesRouteImport.update({ - id: '/web/guides', - path: '/web/guides', - getParentRoute: () => DashboardRoute, + id: '/guides', + path: '/guides', + getParentRoute: () => DashboardWebRoute, +} as any) +const DashboardWebGroupLabelsRoute = DashboardWebGroupLabelsRouteImport.update({ + id: '/group-labels', + path: '/group-labels', + getParentRoute: () => DashboardWebRoute, } as any) const DashboardWebFaqsRoute = DashboardWebFaqsRouteImport.update({ - id: '/web/faqs', - path: '/web/faqs', - getParentRoute: () => DashboardRoute, + id: '/faqs', + path: '/faqs', + getParentRoute: () => DashboardWebRoute, } as any) const DashboardWebAssociationsRoute = DashboardWebAssociationsRouteImport.update({ - id: '/web/associations', - path: '/web/associations', - getParentRoute: () => DashboardRoute, + id: '/associations', + path: '/associations', + getParentRoute: () => DashboardWebRoute, } as any) const DashboardTelegramGroupsRoute = DashboardTelegramGroupsRouteImport.update({ id: '/telegram/groups', @@ -109,12 +129,24 @@ const ApiAuthSplatRoute = ApiAuthSplatRouteImport.update({ path: '/api/auth/$', getParentRoute: () => rootRouteImport, } as any) +const DashboardWebGroupsByLabelIndexRoute = + DashboardWebGroupsByLabelIndexRouteImport.update({ + id: '/groups-by-label/', + path: '/groups-by-label/', + getParentRoute: () => DashboardWebRoute, + } as any) const DashboardTelegramUsersIndexRoute = DashboardTelegramUsersIndexRouteImport.update({ id: '/telegram/users/', path: '/telegram/users/', getParentRoute: () => DashboardRoute, } as any) +const DashboardWebGroupsByLabelSplatRoute = + DashboardWebGroupsByLabelSplatRouteImport.update({ + id: '/groups-by-label/$', + path: '/groups-by-label/$', + getParentRoute: () => DashboardWebRoute, + } as any) const DashboardTelegramUsersUserIdRoute = DashboardTelegramUsersUserIdRouteImport.update({ id: '/telegram/users/$userId', @@ -127,6 +159,7 @@ export interface FileRoutesByFullPath { '/dashboard': typeof DashboardRouteWithChildren '/login': typeof LoginRoute '/dashboard/account': typeof DashboardAccountRoute + '/dashboard/web': typeof DashboardWebRouteWithChildren '/onboarding/link': typeof OnboardingLinkRoute '/onboarding/unauthorized': typeof OnboardingUnauthorizedRoute '/dashboard/': typeof DashboardIndexRoute @@ -137,15 +170,20 @@ export interface FileRoutesByFullPath { '/dashboard/telegram/groups': typeof DashboardTelegramGroupsRoute '/dashboard/web/associations': typeof DashboardWebAssociationsRoute '/dashboard/web/faqs': typeof DashboardWebFaqsRoute + '/dashboard/web/group-labels': typeof DashboardWebGroupLabelsRoute '/dashboard/web/guides': typeof DashboardWebGuidesRoute '/dashboard/web/projects': typeof DashboardWebProjectsRoute + '/dashboard/whatsapp/groups': typeof DashboardWhatsappGroupsRoute '/dashboard/telegram/users/$userId': typeof DashboardTelegramUsersUserIdRoute + '/dashboard/web/groups-by-label/$': typeof DashboardWebGroupsByLabelSplatRoute '/dashboard/telegram/users/': typeof DashboardTelegramUsersIndexRoute + '/dashboard/web/groups-by-label/': typeof DashboardWebGroupsByLabelIndexRoute } export interface FileRoutesByTo { '/': typeof IndexRoute '/login': typeof LoginRoute '/dashboard/account': typeof DashboardAccountRoute + '/dashboard/web': typeof DashboardWebRouteWithChildren '/onboarding/link': typeof OnboardingLinkRoute '/onboarding/unauthorized': typeof OnboardingUnauthorizedRoute '/dashboard': typeof DashboardIndexRoute @@ -156,10 +194,14 @@ export interface FileRoutesByTo { '/dashboard/telegram/groups': typeof DashboardTelegramGroupsRoute '/dashboard/web/associations': typeof DashboardWebAssociationsRoute '/dashboard/web/faqs': typeof DashboardWebFaqsRoute + '/dashboard/web/group-labels': typeof DashboardWebGroupLabelsRoute '/dashboard/web/guides': typeof DashboardWebGuidesRoute '/dashboard/web/projects': typeof DashboardWebProjectsRoute + '/dashboard/whatsapp/groups': typeof DashboardWhatsappGroupsRoute '/dashboard/telegram/users/$userId': typeof DashboardTelegramUsersUserIdRoute + '/dashboard/web/groups-by-label/$': typeof DashboardWebGroupsByLabelSplatRoute '/dashboard/telegram/users': typeof DashboardTelegramUsersIndexRoute + '/dashboard/web/groups-by-label': typeof DashboardWebGroupsByLabelIndexRoute } export interface FileRoutesById { __root__: typeof rootRouteImport @@ -167,6 +209,7 @@ export interface FileRoutesById { '/dashboard': typeof DashboardRouteWithChildren '/login': typeof LoginRoute '/dashboard/account': typeof DashboardAccountRoute + '/dashboard/web': typeof DashboardWebRouteWithChildren '/onboarding/link': typeof OnboardingLinkRoute '/onboarding/unauthorized': typeof OnboardingUnauthorizedRoute '/dashboard/': typeof DashboardIndexRoute @@ -177,10 +220,14 @@ export interface FileRoutesById { '/dashboard/telegram/groups': typeof DashboardTelegramGroupsRoute '/dashboard/web/associations': typeof DashboardWebAssociationsRoute '/dashboard/web/faqs': typeof DashboardWebFaqsRoute + '/dashboard/web/group-labels': typeof DashboardWebGroupLabelsRoute '/dashboard/web/guides': typeof DashboardWebGuidesRoute '/dashboard/web/projects': typeof DashboardWebProjectsRoute + '/dashboard/whatsapp/groups': typeof DashboardWhatsappGroupsRoute '/dashboard/telegram/users/$userId': typeof DashboardTelegramUsersUserIdRoute + '/dashboard/web/groups-by-label/$': typeof DashboardWebGroupsByLabelSplatRoute '/dashboard/telegram/users/': typeof DashboardTelegramUsersIndexRoute + '/dashboard/web/groups-by-label/': typeof DashboardWebGroupsByLabelIndexRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath @@ -189,6 +236,7 @@ export interface FileRouteTypes { | '/dashboard' | '/login' | '/dashboard/account' + | '/dashboard/web' | '/onboarding/link' | '/onboarding/unauthorized' | '/dashboard/' @@ -199,15 +247,20 @@ export interface FileRouteTypes { | '/dashboard/telegram/groups' | '/dashboard/web/associations' | '/dashboard/web/faqs' + | '/dashboard/web/group-labels' | '/dashboard/web/guides' | '/dashboard/web/projects' + | '/dashboard/whatsapp/groups' | '/dashboard/telegram/users/$userId' + | '/dashboard/web/groups-by-label/$' | '/dashboard/telegram/users/' + | '/dashboard/web/groups-by-label/' fileRoutesByTo: FileRoutesByTo to: | '/' | '/login' | '/dashboard/account' + | '/dashboard/web' | '/onboarding/link' | '/onboarding/unauthorized' | '/dashboard' @@ -218,16 +271,21 @@ export interface FileRouteTypes { | '/dashboard/telegram/groups' | '/dashboard/web/associations' | '/dashboard/web/faqs' + | '/dashboard/web/group-labels' | '/dashboard/web/guides' | '/dashboard/web/projects' + | '/dashboard/whatsapp/groups' | '/dashboard/telegram/users/$userId' + | '/dashboard/web/groups-by-label/$' | '/dashboard/telegram/users' + | '/dashboard/web/groups-by-label' id: | '__root__' | '/' | '/dashboard' | '/login' | '/dashboard/account' + | '/dashboard/web' | '/onboarding/link' | '/onboarding/unauthorized' | '/dashboard/' @@ -238,10 +296,14 @@ export interface FileRouteTypes { | '/dashboard/telegram/groups' | '/dashboard/web/associations' | '/dashboard/web/faqs' + | '/dashboard/web/group-labels' | '/dashboard/web/guides' | '/dashboard/web/projects' + | '/dashboard/whatsapp/groups' | '/dashboard/telegram/users/$userId' + | '/dashboard/web/groups-by-label/$' | '/dashboard/telegram/users/' + | '/dashboard/web/groups-by-label/' fileRoutesById: FileRoutesById } export interface RootRouteChildren { @@ -297,6 +359,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof OnboardingLinkRouteImport parentRoute: typeof rootRouteImport } + '/dashboard/web': { + id: '/dashboard/web' + path: '/web' + fullPath: '/dashboard/web' + preLoaderRoute: typeof DashboardWebRouteImport + parentRoute: typeof DashboardRoute + } '/dashboard/account': { id: '/dashboard/account' path: '/account' @@ -304,33 +373,47 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof DashboardAccountRouteImport parentRoute: typeof DashboardRoute } + '/dashboard/whatsapp/groups': { + id: '/dashboard/whatsapp/groups' + path: '/whatsapp/groups' + fullPath: '/dashboard/whatsapp/groups' + preLoaderRoute: typeof DashboardWhatsappGroupsRouteImport + parentRoute: typeof DashboardRoute + } '/dashboard/web/projects': { id: '/dashboard/web/projects' - path: '/web/projects' + path: '/projects' fullPath: '/dashboard/web/projects' preLoaderRoute: typeof DashboardWebProjectsRouteImport - parentRoute: typeof DashboardRoute + parentRoute: typeof DashboardWebRoute } '/dashboard/web/guides': { id: '/dashboard/web/guides' - path: '/web/guides' + path: '/guides' fullPath: '/dashboard/web/guides' preLoaderRoute: typeof DashboardWebGuidesRouteImport - parentRoute: typeof DashboardRoute + parentRoute: typeof DashboardWebRoute + } + '/dashboard/web/group-labels': { + id: '/dashboard/web/group-labels' + path: '/group-labels' + fullPath: '/dashboard/web/group-labels' + preLoaderRoute: typeof DashboardWebGroupLabelsRouteImport + parentRoute: typeof DashboardWebRoute } '/dashboard/web/faqs': { id: '/dashboard/web/faqs' - path: '/web/faqs' + path: '/faqs' fullPath: '/dashboard/web/faqs' preLoaderRoute: typeof DashboardWebFaqsRouteImport - parentRoute: typeof DashboardRoute + parentRoute: typeof DashboardWebRoute } '/dashboard/web/associations': { id: '/dashboard/web/associations' - path: '/web/associations' + path: '/associations' fullPath: '/dashboard/web/associations' preLoaderRoute: typeof DashboardWebAssociationsRouteImport - parentRoute: typeof DashboardRoute + parentRoute: typeof DashboardWebRoute } '/dashboard/telegram/groups': { id: '/dashboard/telegram/groups' @@ -367,6 +450,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ApiAuthSplatRouteImport parentRoute: typeof rootRouteImport } + '/dashboard/web/groups-by-label/': { + id: '/dashboard/web/groups-by-label/' + path: '/groups-by-label' + fullPath: '/dashboard/web/groups-by-label/' + preLoaderRoute: typeof DashboardWebGroupsByLabelIndexRouteImport + parentRoute: typeof DashboardWebRoute + } '/dashboard/telegram/users/': { id: '/dashboard/telegram/users/' path: '/telegram/users' @@ -374,6 +464,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof DashboardTelegramUsersIndexRouteImport parentRoute: typeof DashboardRoute } + '/dashboard/web/groups-by-label/$': { + id: '/dashboard/web/groups-by-label/$' + path: '/groups-by-label/$' + fullPath: '/dashboard/web/groups-by-label/$' + preLoaderRoute: typeof DashboardWebGroupsByLabelSplatRouteImport + parentRoute: typeof DashboardWebRoute + } '/dashboard/telegram/users/$userId': { id: '/dashboard/telegram/users/$userId' path: '/telegram/users/$userId' @@ -384,32 +481,52 @@ declare module '@tanstack/react-router' { } } +interface DashboardWebRouteChildren { + DashboardWebAssociationsRoute: typeof DashboardWebAssociationsRoute + DashboardWebFaqsRoute: typeof DashboardWebFaqsRoute + DashboardWebGroupLabelsRoute: typeof DashboardWebGroupLabelsRoute + DashboardWebGuidesRoute: typeof DashboardWebGuidesRoute + DashboardWebProjectsRoute: typeof DashboardWebProjectsRoute + DashboardWebGroupsByLabelSplatRoute: typeof DashboardWebGroupsByLabelSplatRoute + DashboardWebGroupsByLabelIndexRoute: typeof DashboardWebGroupsByLabelIndexRoute +} + +const DashboardWebRouteChildren: DashboardWebRouteChildren = { + DashboardWebAssociationsRoute: DashboardWebAssociationsRoute, + DashboardWebFaqsRoute: DashboardWebFaqsRoute, + DashboardWebGroupLabelsRoute: DashboardWebGroupLabelsRoute, + DashboardWebGuidesRoute: DashboardWebGuidesRoute, + DashboardWebProjectsRoute: DashboardWebProjectsRoute, + DashboardWebGroupsByLabelSplatRoute: DashboardWebGroupsByLabelSplatRoute, + DashboardWebGroupsByLabelIndexRoute: DashboardWebGroupsByLabelIndexRoute, +} + +const DashboardWebRouteWithChildren = DashboardWebRoute._addFileChildren( + DashboardWebRouteChildren, +) + interface DashboardRouteChildren { DashboardAccountRoute: typeof DashboardAccountRoute + DashboardWebRoute: typeof DashboardWebRouteWithChildren DashboardIndexRoute: typeof DashboardIndexRoute DashboardAzureGroupsRoute: typeof DashboardAzureGroupsRoute DashboardAzureMembersRoute: typeof DashboardAzureMembersRoute DashboardTelegramGrantsRoute: typeof DashboardTelegramGrantsRoute DashboardTelegramGroupsRoute: typeof DashboardTelegramGroupsRoute - DashboardWebAssociationsRoute: typeof DashboardWebAssociationsRoute - DashboardWebFaqsRoute: typeof DashboardWebFaqsRoute - DashboardWebGuidesRoute: typeof DashboardWebGuidesRoute - DashboardWebProjectsRoute: typeof DashboardWebProjectsRoute + DashboardWhatsappGroupsRoute: typeof DashboardWhatsappGroupsRoute DashboardTelegramUsersUserIdRoute: typeof DashboardTelegramUsersUserIdRoute DashboardTelegramUsersIndexRoute: typeof DashboardTelegramUsersIndexRoute } const DashboardRouteChildren: DashboardRouteChildren = { DashboardAccountRoute: DashboardAccountRoute, + DashboardWebRoute: DashboardWebRouteWithChildren, DashboardIndexRoute: DashboardIndexRoute, DashboardAzureGroupsRoute: DashboardAzureGroupsRoute, DashboardAzureMembersRoute: DashboardAzureMembersRoute, DashboardTelegramGrantsRoute: DashboardTelegramGrantsRoute, DashboardTelegramGroupsRoute: DashboardTelegramGroupsRoute, - DashboardWebAssociationsRoute: DashboardWebAssociationsRoute, - DashboardWebFaqsRoute: DashboardWebFaqsRoute, - DashboardWebGuidesRoute: DashboardWebGuidesRoute, - DashboardWebProjectsRoute: DashboardWebProjectsRoute, + DashboardWhatsappGroupsRoute: DashboardWhatsappGroupsRoute, DashboardTelegramUsersUserIdRoute: DashboardTelegramUsersUserIdRoute, DashboardTelegramUsersIndexRoute: DashboardTelegramUsersIndexRoute, } diff --git a/src/routes/dashboard/azure/groups.tsx b/src/routes/dashboard/azure/groups.tsx index 8286cf2..fd64f60 100644 --- a/src/routes/dashboard/azure/groups.tsx +++ b/src/routes/dashboard/azure/groups.tsx @@ -2,6 +2,7 @@ import { createFileRoute } from "@tanstack/react-router" import { getAzureDirectory } from "@/features/azure/azure.functions" import { AzureGroupsPage, AzureGroupsSkeleton } from "@/features/azure/groups-page" +import { hasWriteAdminRole } from "@/server/authorization" export const Route = createFileRoute("/dashboard/azure/groups")({ loader: () => getAzureDirectory(), @@ -12,5 +13,6 @@ export const Route = createFileRoute("/dashboard/azure/groups")({ function AzureGroupsRoute() { const { groups, members } = Route.useLoaderData() - return + const { roles } = Route.useRouteContext() + return } diff --git a/src/routes/dashboard/azure/members.tsx b/src/routes/dashboard/azure/members.tsx index 370310d..c175fc0 100644 --- a/src/routes/dashboard/azure/members.tsx +++ b/src/routes/dashboard/azure/members.tsx @@ -3,6 +3,7 @@ import { createFileRoute } from "@tanstack/react-router" import { DataPageSkeleton } from "@/components/loading-skeleton" import { getAzureMembers } from "@/features/azure/azure.functions" import { AzureMembersPage } from "@/features/azure/members-page" +import { hasWriteAdminRole } from "@/server/authorization" export const Route = createFileRoute("/dashboard/azure/members")({ loader: () => getAzureMembers(), @@ -12,5 +13,6 @@ export const Route = createFileRoute("/dashboard/azure/members")({ function AzureMembersRoute() { const members = Route.useLoaderData() - return + const { roles } = Route.useRouteContext() + return } diff --git a/src/routes/dashboard/telegram/grants.tsx b/src/routes/dashboard/telegram/grants.tsx index db016a0..ba6f455 100644 --- a/src/routes/dashboard/telegram/grants.tsx +++ b/src/routes/dashboard/telegram/grants.tsx @@ -3,6 +3,7 @@ import { createFileRoute } from "@tanstack/react-router" import { DataPageSkeleton } from "@/components/loading-skeleton" import { TelegramGrantsPage } from "@/features/telegram/grants-page" import { getTelegramGrants } from "@/features/telegram/grants.functions" +import { hasWriteAdminRole } from "@/server/authorization" export const Route = createFileRoute("/dashboard/telegram/grants")({ loader: () => getTelegramGrants(), @@ -12,5 +13,6 @@ export const Route = createFileRoute("/dashboard/telegram/grants")({ function TelegramGrantsRoute() { const grants = Route.useLoaderData() - return + const { roles } = Route.useRouteContext() + return } diff --git a/src/routes/dashboard/telegram/groups.tsx b/src/routes/dashboard/telegram/groups.tsx index 4a70823..4208d74 100644 --- a/src/routes/dashboard/telegram/groups.tsx +++ b/src/routes/dashboard/telegram/groups.tsx @@ -1,16 +1,30 @@ import { createFileRoute } from "@tanstack/react-router" import { DataPageSkeleton } from "@/components/loading-skeleton" +import { listGroupLabels, listGroupsWithLabels } from "@/features/group-labels/group-labels.functions" import { TelegramGroupsPage } from "@/features/telegram/groups-page" import { getTelegramGroups } from "@/features/telegram/groups.functions" export const Route = createFileRoute("/dashboard/telegram/groups")({ - loader: () => getTelegramGroups(), + loader: async () => { + const [groups, groupLabels, groupsWithLabels] = await Promise.all([ + getTelegramGroups(), + listGroupLabels(), + listGroupsWithLabels(), + ]) + return { groups, groupLabels, groupsWithLabels } + }, pendingComponent: () => , component: TelegramGroupsRoute, }) function TelegramGroupsRoute() { - const groups = Route.useLoaderData() - return + const { groups, groupLabels, groupsWithLabels } = Route.useLoaderData() + return ( + + ) } diff --git a/src/routes/dashboard/telegram/users/$userId.tsx b/src/routes/dashboard/telegram/users/$userId.tsx index 84273ca..28e9139 100644 --- a/src/routes/dashboard/telegram/users/$userId.tsx +++ b/src/routes/dashboard/telegram/users/$userId.tsx @@ -4,6 +4,7 @@ import { DetailPageSkeleton } from "@/components/loading-skeleton" import { TelegramUserProfile } from "@/features/telegram/user-detail/profile" import { UserDetailBackLink } from "@/features/telegram/user-detail/sections" import { getTelegramUserDetails } from "@/features/telegram/users.functions" +import { hasWriteAdminRole } from "@/server/authorization" export const Route = createFileRoute("/dashboard/telegram/users/$userId")({ loader: ({ params }) => { @@ -17,10 +18,11 @@ export const Route = createFileRoute("/dashboard/telegram/users/$userId")({ function UserProfileRoute() { const data = Route.useLoaderData() + const { roles } = Route.useRouteContext() return (
- +
) } diff --git a/src/routes/dashboard/web.tsx b/src/routes/dashboard/web.tsx new file mode 100644 index 0000000..b36d846 --- /dev/null +++ b/src/routes/dashboard/web.tsx @@ -0,0 +1,10 @@ +import { Outlet, createFileRoute, redirect } from "@tanstack/react-router" + +import { hasWebAdminRole } from "@/server/authorization" + +export const Route = createFileRoute("/dashboard/web")({ + beforeLoad: ({ context }) => { + if (!hasWebAdminRole(context.roles)) throw redirect({ to: "/onboarding/unauthorized" }) + }, + component: Outlet, +}) diff --git a/src/routes/dashboard/web/group-labels.tsx b/src/routes/dashboard/web/group-labels.tsx new file mode 100644 index 0000000..a5c8cbf --- /dev/null +++ b/src/routes/dashboard/web/group-labels.tsx @@ -0,0 +1,15 @@ +import { createFileRoute } from "@tanstack/react-router" + +import { DataPageSkeleton } from "@/components/loading-skeleton" +import { GroupLabelsPage } from "@/features/group-labels/group-labels-page" +import { listGroupLabels } from "@/features/group-labels/group-labels.functions" + +export const Route = createFileRoute("/dashboard/web/group-labels")({ + loader: () => listGroupLabels(), + pendingComponent: () => , + component: GroupLabelsRoute, +}) + +function GroupLabelsRoute() { + return +} diff --git a/src/routes/dashboard/web/groups-by-label/$.tsx b/src/routes/dashboard/web/groups-by-label/$.tsx new file mode 100644 index 0000000..cb1adf9 --- /dev/null +++ b/src/routes/dashboard/web/groups-by-label/$.tsx @@ -0,0 +1,39 @@ +import { createFileRoute } from "@tanstack/react-router" + +import { DataPageSkeleton } from "@/components/loading-skeleton" +import { + listGroupLabels, + listGroupsForLabels, + listGroupsWithLabels, +} from "@/features/group-labels/group-labels.functions" +import { urlSegmentsToLabelPath } from "@/features/group-labels/label-tree" +import { GroupsByLabelPage } from "@/features/groups-by-label/groups-by-label-page" + +export const Route = createFileRoute("/dashboard/web/groups-by-label/$")({ + loader: async () => { + const [groups, groupLabels, groupsWithLabels] = await Promise.all([ + listGroupsForLabels(), + listGroupLabels(), + listGroupsWithLabels(), + ]) + return { ...groups, groupLabels, groupsWithLabels } + }, + pendingComponent: () => , + component: GroupsByLabelRoute, +}) + +function GroupsByLabelRoute() { + const { _splat } = Route.useParams() + const { tgGroups, groupLabels, groupsWithLabels, waGroups } = Route.useLoaderData() + const path = urlSegmentsToLabelPath((_splat ?? "").split("/")) + + return ( + + ) +} diff --git a/src/routes/dashboard/web/groups-by-label/index.tsx b/src/routes/dashboard/web/groups-by-label/index.tsx new file mode 100644 index 0000000..c4752cd --- /dev/null +++ b/src/routes/dashboard/web/groups-by-label/index.tsx @@ -0,0 +1,37 @@ +import { createFileRoute } from "@tanstack/react-router" + +import { DataPageSkeleton } from "@/components/loading-skeleton" +import { + listGroupLabels, + listGroupsForLabels, + listGroupsWithLabels, +} from "@/features/group-labels/group-labels.functions" +import { GroupsByLabelPage } from "@/features/groups-by-label/groups-by-label-page" + +export const Route = createFileRoute("/dashboard/web/groups-by-label/")({ + loader: async () => { + const [groups, groupLabels, groupsWithLabels] = await Promise.all([ + listGroupsForLabels(), + listGroupLabels(), + listGroupsWithLabels(), + ]) + return { ...groups, groupLabels, groupsWithLabels } + }, + pendingComponent: () => , + component: GroupsByLabelRoot, +}) + +/** The top of the category tree — picks Didattica or Extra to drill into, no exact-match groups of its own. */ +function GroupsByLabelRoot() { + const { tgGroups, groupLabels, groupsWithLabels, waGroups } = Route.useLoaderData() + + return ( + + ) +} diff --git a/src/routes/dashboard/whatsapp/groups.tsx b/src/routes/dashboard/whatsapp/groups.tsx new file mode 100644 index 0000000..20f2cca --- /dev/null +++ b/src/routes/dashboard/whatsapp/groups.tsx @@ -0,0 +1,30 @@ +import { createFileRoute } from "@tanstack/react-router" + +import { DataPageSkeleton } from "@/components/loading-skeleton" +import { listGroupLabels, listGroupsWithLabels } from "@/features/group-labels/group-labels.functions" +import { getWhatsappGroups } from "@/features/whatsapp/groups.functions" +import { WhatsappGroupsPage } from "@/features/whatsapp/whatsapp-groups-page" + +export const Route = createFileRoute("/dashboard/whatsapp/groups")({ + loader: async () => { + const [groups, groupLabels, groupsWithLabels] = await Promise.all([ + getWhatsappGroups(), + listGroupLabels(), + listGroupsWithLabels(), + ]) + return { groups, groupLabels, groupsWithLabels } + }, + pendingComponent: () => , + component: WhatsappGroupsRoute, +}) + +function WhatsappGroupsRoute() { + const { groups, groupLabels, groupsWithLabels } = Route.useLoaderData() + return ( + + ) +} diff --git a/src/server/auth.middleware.ts b/src/server/auth.middleware.ts index 79a01b2..9b97c7d 100644 --- a/src/server/auth.middleware.ts +++ b/src/server/auth.middleware.ts @@ -2,10 +2,13 @@ import { redirect } from "@tanstack/react-router" import { createMiddleware } from "@tanstack/react-start" import type { AdminSession } from "@/lib/auth" - type DashboardAccess = | { status: "unauthenticated"; session: null; roles: string[] } - | { status: "telegram-unlinked" | "forbidden" | "authorized"; session: AdminSession; roles: string[] } + | { + status: "telegram-unlinked" | "forbidden" | "authorized" + session: AdminSession + roles: string[] + } export const backendMiddleware = createMiddleware({ type: "function" }).server(async ({ next }) => { const { createRequestBackend } = await import("@/server/auth.server") @@ -31,8 +34,8 @@ export const dashboardAccessMiddleware = createMiddleware({ type: "function" }) if (!context.session) { dashboardAccess = { status: "unauthenticated", session: null, roles: [] } } else { - const { authorizeAdmin } = await import("@/server/auth.server") - const authorization = await authorizeAdmin(context.session, context.backend) + const { authorizeWebAdmin } = await import("@/server/auth.server") + const authorization = await authorizeWebAdmin(context.session, context.backend) if (authorization === "telegram-unlinked") { dashboardAccess = { status: "telegram-unlinked", session: context.session, roles: [] } } else if (authorization === "forbidden") { @@ -66,6 +69,17 @@ export const adminMiddleware = createMiddleware({ type: "function" }) return next({ context: authorization }) }) +/** Authorization for reads and writes that are exclusively exposed under /dashboard/web. */ +export const webAdminMiddleware = createMiddleware({ type: "function" }) + .middleware([authenticatedMiddleware]) + .server(async ({ next, context }) => { + const { authorizeWebAdmin } = await import("@/server/auth.server") + const authorization = await authorizeWebAdmin(context.session, context.backend) + if (authorization === "telegram-unlinked") throw redirect({ to: "/onboarding/link" }) + if (authorization === "forbidden") throw redirect({ to: "/onboarding/unauthorized" }) + return next({ context: authorization }) + }) + export const writeAdminMiddleware = createMiddleware({ type: "function" }) .middleware([adminMiddleware]) .server(async ({ next, context }) => { @@ -73,3 +87,21 @@ export const writeAdminMiddleware = createMiddleware({ type: "function" }) if (!hasWriteAdminRole(context.roles)) throw new Error("UNAUTHORIZED") return next() }) + +/** Group management is writable by full write administrators and the dedicated web role. */ +export const groupWriteAdminMiddleware = createMiddleware({ type: "function" }) + .middleware([adminMiddleware]) + .server(async ({ next, context }) => { + const { hasGroupWriteRole } = await import("@/server/authorization") + if (!hasGroupWriteRole(context.roles)) throw new Error("UNAUTHORIZED") + return next() + }) + +/** For mutations scoped to the /dashboard/web section: full write roles, plus the "web" role. */ +export const webWriteAdminMiddleware = createMiddleware({ type: "function" }) + .middleware([webAdminMiddleware]) + .server(async ({ next, context }) => { + const { hasWebWriteRole } = await import("@/server/authorization") + if (!hasWebWriteRole(context.roles)) throw new Error("UNAUTHORIZED") + return next() + }) diff --git a/src/server/auth.server.ts b/src/server/auth.server.ts index 1eb5b19..21f6945 100644 --- a/src/server/auth.server.ts +++ b/src/server/auth.server.ts @@ -5,7 +5,7 @@ import { createAuthClient } from "better-auth/client" import { env } from "@/env" import type { AdminSession } from "@/lib/auth" import { createAuthPlugins } from "@/lib/auth-plugins" -import { ADMIN_ROLES, hasAdminRole, isAgentModeEnabled } from "@/server/authorization" +import { ADMIN_ROLES, hasAdminRole, hasWebAdminRole, isAgentModeEnabled } from "@/server/authorization" import { type BackendClient, createBackendClient } from "@/server/backend.server" const serverAuth = createAuthClient({ @@ -67,9 +67,10 @@ export type AdminAuthorization = { roles: string[] } -export async function authorizeAdmin( +async function authorizeWithRole( session: AdminSession, - backend: BackendClient + backend: BackendClient, + hasRole: (roles: readonly string[]) => boolean ): Promise { const telegramId = session.user.telegramId if (!telegramId) return "telegram-unlinked" @@ -78,6 +79,14 @@ export async function authorizeAdmin( ? [...ADMIN_ROLES] : ((await backend.tg.permissions.getRoles.query({ userId: telegramId })).roles ?? []) - if (!hasAdminRole(roles)) return "forbidden" + if (!hasRole(roles)) return "forbidden" return { session, telegramId, roles } } + +export function authorizeAdmin(session: AdminSession, backend: BackendClient) { + return authorizeWithRole(session, backend, hasAdminRole) +} + +export function authorizeWebAdmin(session: AdminSession, backend: BackendClient) { + return authorizeWithRole(session, backend, hasWebAdminRole) +} diff --git a/src/server/authorization.ts b/src/server/authorization.ts index a5424ca..687b8bb 100644 --- a/src/server/authorization.ts +++ b/src/server/authorization.ts @@ -1,4 +1,4 @@ -export const ADMIN_ROLES = ["owner", "direttivo", "president", "hr"] as const +export const ADMIN_ROLES = ["owner", "direttivo", "president", "hr", "web"] as const export const WRITE_ADMIN_ROLES = ["owner", "direttivo", "president"] as const export function isAgentModeEnabled(nodeEnv: string, agentMode: boolean) { @@ -14,3 +14,20 @@ export function hasWriteAdminRole(roles: readonly string[]) { if (roles.includes("creator")) return false return roles.some((role) => WRITE_ADMIN_ROLES.some((adminRole) => adminRole === role)) } + +/** Roles that may access the web-content section. All dashboard readers currently qualify. */ +export function hasWebAdminRole(roles: readonly string[]) { + return hasAdminRole(roles) +} + +/** Full write roles, plus "web" — but only for mutations scoped to the /dashboard/web section. */ +export function hasWebWriteRole(roles: readonly string[]) { + if (roles.includes("creator")) return false + return hasWriteAdminRole(roles) || roles.includes("web") +} + +/** Group operations are editable by full write administrators and the dedicated web role. */ +export function hasGroupWriteRole(roles: readonly string[]) { + if (roles.includes("creator")) return false + return hasWriteAdminRole(roles) || roles.includes("web") +} diff --git a/tests/server-security.test.mjs b/tests/server-security.test.mjs index 490c092..1c71d25 100644 --- a/tests/server-security.test.mjs +++ b/tests/server-security.test.mjs @@ -10,10 +10,19 @@ import { associationSaveErrorMessage, parseCreateAssociationForm, } from "../src/features/associations/associations.validation.ts" +import { isValidLabelSegment } from "../src/features/group-labels/label-tree.ts" import { parseGuideForm } from "../src/features/guides/guides.validation.ts" import { parseProjectForm, projectSaveErrorMessage } from "../src/features/projects/projects.validation.ts" +import { isValidWhatsappInviteLink } from "../src/features/whatsapp/whatsapp.validation.ts" import { forwardAuthRequest } from "../src/server/auth-proxy-core.ts" -import { hasAdminRole, hasWriteAdminRole, isAgentModeEnabled } from "../src/server/authorization.ts" +import { + hasAdminRole, + hasGroupWriteRole, + hasWebAdminRole, + hasWebWriteRole, + hasWriteAdminRole, + isAgentModeEnabled, +} from "../src/server/authorization.ts" import { getForwardedCookieHeaders } from "../src/server/request-headers.ts" import { resolveBackendUrl } from "../src/server/runtime-env.ts" @@ -139,16 +148,49 @@ test("the backend URL only falls back during development or compilation", () => assert.equal(resolveBackendUrl(undefined, undefined, undefined), undefined) }) -test("only dashboard administrator roles authorize access", () => { +test("dashboard reader roles authorize access", () => { assert.equal(hasAdminRole(["owner"]), true) assert.equal(hasAdminRole(["direttivo"]), true) assert.equal(hasAdminRole(["president"]), true) assert.equal(hasAdminRole(["hr"]), true) + assert.equal(hasAdminRole(["web"]), true) assert.equal(hasAdminRole(["creator"]), false) assert.equal(hasAdminRole(["creator", "owner"]), false) assert.equal(hasAdminRole([]), false) }) +test("the web role can write web content and groups, but not restricted administration areas", () => { + assert.equal(hasWebAdminRole(["web"]), true) + assert.equal(hasWebWriteRole(["web"]), true) + assert.equal(hasGroupWriteRole(["web"]), true) + assert.equal(hasWriteAdminRole(["web"]), false) + assert.equal(hasGroupWriteRole(["hr"]), false) + assert.equal(hasGroupWriteRole(["owner"]), true) + assert.equal(hasWebAdminRole(["creator", "web"]), false) + assert.equal(hasGroupWriteRole(["creator", "web"]), false) +}) + +test("group label paths reject URL separators", async () => { + assert.equal(isValidLabelSegment("machine/learning"), false) + assert.equal(isValidLabelSegment("machine?learning"), false) + assert.equal(isValidLabelSegment("machine-learning"), true) + const validationSource = await readFile( + new URL("../src/features/group-labels/group-labels.validation.ts", import.meta.url), + "utf8" + ) + assert.match(validationSource, /segments\.some\(\(segment\) => !isValidLabelSegment\(segment\)\)/) +}) + +test("WhatsApp group links require a real HTTPS invite code", () => { + assert.equal(isValidWhatsappInviteLink("https://chat.whatsapp.com/AbCdEf123456"), true) + assert.equal(isValidWhatsappInviteLink("https://chat.whatsapp.com/AbCdEf123456?mode=ems_copy_t"), true) + assert.equal(isValidWhatsappInviteLink("https://chat.whatsapp.com/"), false) + assert.equal(isValidWhatsappInviteLink("https://chat.whatsapp.com/two/segments"), false) + assert.equal(isValidWhatsappInviteLink("http://chat.whatsapp.com/AbCdEf123456"), false) + assert.equal(isValidWhatsappInviteLink("https://example.com/AbCdEf123456"), false) + assert.equal(isValidWhatsappInviteLink("not a URL"), false) +}) + test("HR dashboard access is read-only", () => { assert.equal(hasWriteAdminRole(["owner"]), true) assert.equal(hasWriteAdminRole(["direttivo"]), true) @@ -197,15 +239,18 @@ test("the auth proxy preserves the request and exact upstream response", async ( ]) }) -test("admin server functions attach the authorization middleware", async () => { +test("dashboard server functions attach their scoped authorization middleware", async () => { const adminFunctionFiles = [ "src/features/associations/associations.functions.ts", "src/features/azure/azure.functions.ts", "src/features/guides/guides.functions.ts", "src/features/projects/projects.functions.ts", + "src/features/group-labels/group-labels.functions.ts", + "src/features/faqs/faqs.functions.ts", "src/features/telegram/grants.functions.ts", "src/features/telegram/groups.functions.ts", "src/features/telegram/users.functions.ts", + "src/features/whatsapp/groups.functions.ts", ] for (const file of adminFunctionFiles) { @@ -216,32 +261,47 @@ test("admin server functions attach the authorization middleware", async () => { assert.ok(serverFunction.isExported, `${file}:${serverFunction.name} must be exported`) assert.ok( serverFunction.middleware.includes("adminMiddleware") || - serverFunction.middleware.includes("writeAdminMiddleware"), + serverFunction.middleware.includes("writeAdminMiddleware") || + serverFunction.middleware.includes("groupWriteAdminMiddleware") || + serverFunction.middleware.includes("webAdminMiddleware") || + serverFunction.middleware.includes("webWriteAdminMiddleware"), `${file}:${serverFunction.name} must authorize access` ) } } }) -test("dashboard mutations require a write-capable role", async () => { - const adminFunctionFiles = [ - "src/features/associations/associations.functions.ts", +test("dashboard mutations enforce their exact write scope", async () => { + const restrictedMutationFiles = [ "src/features/azure/azure.functions.ts", - "src/features/guides/guides.functions.ts", - "src/features/projects/projects.functions.ts", "src/features/telegram/grants.functions.ts", - "src/features/telegram/groups.functions.ts", "src/features/telegram/users.functions.ts", ] + const groupMutationFiles = ["src/features/telegram/groups.functions.ts", "src/features/whatsapp/groups.functions.ts"] + const webMutationFiles = [ + "src/features/associations/associations.functions.ts", + "src/features/guides/guides.functions.ts", + "src/features/projects/projects.functions.ts", + "src/features/group-labels/group-labels.functions.ts", + "src/features/faqs/faqs.functions.ts", + ] - for (const file of adminFunctionFiles) { - const source = await readFile(new URL(`../${file}`, import.meta.url), "utf8") - const mutations = exportedServerFunctions(source, file).filter((serverFunction) => serverFunction.isPost) - for (const mutation of mutations) { - assert.ok( - mutation.middleware.includes("writeAdminMiddleware"), - `${file}:${mutation.name} must protect mutations with write access` - ) + for (const { expectedMiddleware, files } of [ + { expectedMiddleware: "writeAdminMiddleware", files: restrictedMutationFiles }, + { expectedMiddleware: "groupWriteAdminMiddleware", files: groupMutationFiles }, + { expectedMiddleware: "webWriteAdminMiddleware", files: webMutationFiles }, + ]) { + for (const file of files) { + const source = await readFile(new URL(`../${file}`, import.meta.url), "utf8") + const mutations = exportedServerFunctions(source, file).filter((serverFunction) => serverFunction.isPost) + assert.ok(mutations.length > 0, `${file} must export at least one mutation`) + for (const mutation of mutations) { + assert.deepEqual( + mutation.middleware, + [expectedMiddleware], + `${file}:${mutation.name} must use ${expectedMiddleware}` + ) + } } } }) @@ -251,6 +311,7 @@ test("session middleware marks identity-dependent responses private", async () = assert.match(source, /setResponseHeader\("Cache-Control", "private, no-store"\)/) assert.match(source, /setResponseHeader\("Vary", "Cookie"\)/) assert.match(source, /if \(!hasWriteAdminRole\(context\.roles\)\) throw new Error\("UNAUTHORIZED"\)/) + assert.match(source, /if \(!hasGroupWriteRole\(context\.roles\)\) throw new Error\("UNAUTHORIZED"\)/) assert.doesNotMatch(source, /new Error\("TELEGRAM_NOT_LINKED"\)/) }) @@ -264,7 +325,7 @@ test("event handlers integrate protected server-function redirects with the rout "src/features/azure/member-dialog.tsx": ["createAzureMember", "setAzureMemberNumber"], "src/features/guides/guide-dialogs.tsx": ["createGuide", "deleteGuide"], "src/features/projects/projects-page.tsx": ["createProject", "deleteProject", "editProject", "reorderProjects"], - "src/features/telegram/groups-page.tsx": ["setGroupVisibility"], + "src/features/telegram/groups-table.tsx": ["setGroupVisibility"], "src/features/telegram/leave-group-dialog.tsx": ["leaveTelegramGroup"], "src/features/telegram/user-detail/grant-dialogs.tsx": ["interruptTelegramGrant"], "src/features/telegram/user-detail/group-admin-dialog.tsx": ["addTelegramGroupAdmin", "removeTelegramGroupAdmin"],