diff --git a/README.md b/README.md index 6904360..5283f12 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ -# Aethel Bot +# Aethel Monorepo + +This monorepo includes both the frontend and backend of the Aethel Discord bot. [![Node.js](https://img.shields.io/badge/node-%3E=16.9.0-green?logo=node.js)](https://nodejs.org/) diff --git a/locales/en-US.json b/locales/en-US.json index 4c51fb8..89c051c 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -197,6 +197,48 @@ "done_title": "Done", "none": "—" }, + "cobalt": { + "name": "cobalt", + "description": "Download a video or audio from a given URL", + "options": { + "url": { + "name": "url", + "description": "The URL of the video to download" + }, + "video_quality": { + "name": "video-quality", + "description": "The video quality to download" + }, + "audio_only": { + "name": "audio-only", + "description": "Download audio only" + }, + "mute_audio": { + "name": "mute-audio", + "description": "Mute audio" + }, + "twitter_gif": { + "name": "twitter-gif", + "description": "Download as Twitter GIF" + }, + "tiktok_original_audio": { + "name": "tiktok-original-audio", + "description": "Include TikTok original audio" + }, + "audio_format": { + "name": "audio-format", + "description": "Format for audio (requires audio-only to be true)" + } + }, + "button_label": "Download", + "success": "āœ… **Download ready!**\n\nšŸ”— {url}\nšŸ“„ **Click the button below to download your file.**", + "error": "āŒ **Error:** {error}", + "unknown_error": "An unknown error occurred.", + "rate_limit": "ā³ **Rate limit exceeded.** Please try again later.", + "unknown_response": "ā“ **Unknown response from the API.** Please try again.", + "multiple_items": "šŸ“‹ **Multiple items found for this URL.** Please provide a more specific URL.", + "local_processing_not_supported": "āš ļø **Local processing is not supported.** Please try a different URL or option." + }, "time": { "name": "time", "description": "Get the current time for a timezone", diff --git a/package.json b/package.json index e3ead92..302c761 100644 --- a/package.json +++ b/package.json @@ -19,17 +19,21 @@ }, "dependencies": { "@discordjs/rest": "^2.5.1", + "axios": "^1.10.0", "city-timezones": "^1.3.1", "cors": "^2.8.5", "discord.js": "^14.21.0", "dotenv": "^16.6.1", "eslint-plugin-prettier": "^5.5.1", - "express": "^5.1.0", + "express": "^4.21.2", "express-rate-limit": "^7.5.1", + "express-validator": "^7.2.1", "helmet": "^8.1.0", + "jsonwebtoken": "^9.0.2", "moment-timezone": "^0.6.0", "node-fetch": "^3.3.2", "pg": "^8.16.3", + "uuid": "^11.1.0", "validator": "^13.15.15", "whois-json": "^2.0.4", "winston": "^3.17.0" @@ -37,9 +41,11 @@ "devDependencies": { "@eslint/js": "^9.30.0", "@types/cors": "^2.8.19", - "@types/express": "^5.0.3", + "@types/express": "^4.17.23", + "@types/jsonwebtoken": "^9.0.10", "@types/node": "^24.0.7", "@types/pg": "^8.15.4", + "@types/uuid": "^10.0.0", "@types/validator": "^13.15.2", "@types/whois-json": "^2.0.4", "eslint": "^9.30.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ac7a79e..b19bd44 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@discordjs/rest': specifier: ^2.5.1 version: 2.5.1 + axios: + specifier: ^1.10.0 + version: 1.10.0 city-timezones: specifier: ^1.3.1 version: 1.3.1 @@ -27,14 +30,20 @@ importers: specifier: ^5.5.1 version: 5.5.1(eslint-config-prettier@10.1.5(eslint@9.30.1))(eslint@9.30.1)(prettier@3.6.2) express: - specifier: ^5.1.0 - version: 5.1.0 + specifier: ^4.21.2 + version: 4.21.2 express-rate-limit: specifier: ^7.5.1 - version: 7.5.1(express@5.1.0) + version: 7.5.1(express@4.21.2) + express-validator: + specifier: ^7.2.1 + version: 7.2.1 helmet: specifier: ^8.1.0 version: 8.1.0 + jsonwebtoken: + specifier: ^9.0.2 + version: 9.0.2 moment-timezone: specifier: ^0.6.0 version: 0.6.0 @@ -44,6 +53,9 @@ importers: pg: specifier: ^8.16.3 version: 8.16.3 + uuid: + specifier: ^11.1.0 + version: 11.1.0 validator: specifier: ^13.15.15 version: 13.15.15 @@ -61,14 +73,20 @@ importers: specifier: ^2.8.19 version: 2.8.19 '@types/express': - specifier: ^5.0.3 - version: 5.0.3 + specifier: ^4.17.23 + version: 4.17.23 + '@types/jsonwebtoken': + specifier: ^9.0.10 + version: 9.0.10 '@types/node': specifier: ^24.0.7 version: 24.0.12 '@types/pg': specifier: ^8.15.4 version: 8.15.4 + '@types/uuid': + specifier: ^10.0.0 + version: 10.0.0 '@types/validator': specifier: ^13.15.2 version: 13.15.2 @@ -405,11 +423,11 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - '@types/express-serve-static-core@5.0.7': - resolution: {integrity: sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ==} + '@types/express-serve-static-core@4.19.6': + resolution: {integrity: sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==} - '@types/express@5.0.3': - resolution: {integrity: sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==} + '@types/express@4.17.23': + resolution: {integrity: sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==} '@types/http-errors@2.0.5': resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} @@ -417,9 +435,15 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/jsonwebtoken@9.0.10': + resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} + '@types/mime@1.3.5': resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/node@24.0.12': resolution: {integrity: sha512-LtOrbvDf5ndC9Xi+4QZjVL0woFymF/xSTKZKPgrrl7H7XoeDvnD+E2IclKVDyaK9UM756W/3BXqSU+JEHopA9g==} @@ -441,6 +465,9 @@ packages: '@types/triple-beam@1.3.5': resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} + '@types/uuid@10.0.0': + resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} + '@types/validator@13.15.2': resolution: {integrity: sha512-y7pa/oEJJ4iGYBxOpfAKn5b9+xuihvzDVnC/OSvlVnGxVg0pOqmjiMafiJ1KVNQEaPZf9HsEp5icEwGg8uIe5Q==} @@ -513,8 +540,8 @@ packages: resolution: {integrity: sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA==} engines: {node: '>=v14.0.0', npm: '>=7.0.0'} - accepts@2.0.0: - resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} acorn-jsx@5.3.2: @@ -545,6 +572,9 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + array-union@2.1.0: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} @@ -552,6 +582,12 @@ packages: async@3.2.6: resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axios@1.10.0: + resolution: {integrity: sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -559,9 +595,9 @@ packages: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} - body-parser@2.2.0: - resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} - engines: {node: '>=18'} + body-parser@1.20.3: + resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} @@ -573,6 +609,9 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -635,6 +674,10 @@ packages: colorspace@1.1.4: resolution: {integrity: sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + commander@9.5.0: resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} engines: {node: ^12.20.0 || >=14} @@ -645,20 +688,19 @@ packages: constant-case@2.0.0: resolution: {integrity: sha512-eS0N9WwmjTqrOmR3o83F5vW8Z+9R1HnVz3xmzT2PMFug9ly+Au/fxRWlEBSb6LcZwspSsEn9Xs1uw9YgzAg1EQ==} - content-disposition@1.0.0: - resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==} + content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} content-type@1.0.5: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} - cookie-signature@1.2.2: - resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} - engines: {node: '>=6.6.0'} + cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} - cookie@0.7.2: - resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + cookie@0.7.1: + resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} engines: {node: '>= 0.6'} cors@2.8.5: @@ -673,6 +715,14 @@ packages: resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} engines: {node: '>= 12'} + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + debug@4.4.1: resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} engines: {node: '>=6.0'} @@ -692,10 +742,18 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -718,6 +776,9 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} @@ -727,6 +788,10 @@ packages: enabled@2.0.0: resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==} + encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + encodeurl@2.0.0: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} @@ -743,6 +808,10 @@ packages: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + esbuild@0.25.6: resolution: {integrity: sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==} engines: {node: '>=18'} @@ -827,9 +896,13 @@ packages: peerDependencies: express: '>= 4.11' - express@5.1.0: - resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} - engines: {node: '>= 18'} + express-validator@7.2.1: + resolution: {integrity: sha512-CjNE6aakfpuwGaHQZ3m8ltCG2Qvivd7RHtVMS/6nVxOM7xVGqr4bhflsm4+N5FP5zI7Zxp+Hae+9RE+o8e3ZOQ==} + engines: {node: '>= 8.0.0'} + + express@4.21.2: + resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} + engines: {node: '>= 0.10.0'} fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -865,8 +938,8 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} - finalhandler@2.1.0: - resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} + finalhandler@1.3.1: + resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} engines: {node: '>= 0.8'} find-up@4.1.0: @@ -887,6 +960,19 @@ packages: fn.name@1.1.0: resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} + follow-redirects@1.15.9: + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.4: + resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} + engines: {node: '>= 6'} + formdata-polyfill@4.0.10: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} @@ -895,9 +981,9 @@ packages: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} - fresh@2.0.0: - resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} - engines: {node: '>= 0.8'} + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} @@ -961,6 +1047,10 @@ packages: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} @@ -979,8 +1069,8 @@ packages: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} - iconv-lite@0.6.3: - resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} ignore-by-default@1.0.1: @@ -1039,9 +1129,6 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} - is-promise@4.0.0: - resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} - is-stream@2.0.1: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} @@ -1073,6 +1160,16 @@ packages: engines: {node: '>=6'} hasBin: true + jsonwebtoken@9.0.2: + resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} + engines: {node: '>=12', npm: '>=6'} + + jwa@1.4.2: + resolution: {integrity: sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==} + + jws@3.2.2: + resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -1091,9 +1188,30 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + lodash.snakecase@4.1.1: resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} @@ -1117,30 +1235,38 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} - media-typer@1.1.0: - resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} - engines: {node: '>= 0.8'} + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} - merge-descriptors@2.0.0: - resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} - engines: {node: '>=18'} + merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} - mime-db@1.54.0: - resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} - mime-types@3.0.1: - resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -1157,6 +1283,9 @@ packages: moment@2.30.1: resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -1167,8 +1296,8 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - negotiator@1.0.0: - resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} engines: {node: '>= 0.6'} no-case@2.3.2: @@ -1204,9 +1333,6 @@ packages: 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==} - one-time@1.0.0: resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==} @@ -1259,9 +1385,8 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} - path-to-regexp@8.2.0: - resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==} - engines: {node: '>=16'} + path-to-regexp@0.1.12: + resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} @@ -1342,6 +1467,9 @@ packages: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + pstree.remy@1.1.8: resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} @@ -1349,8 +1477,8 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - qs@6.14.0: - resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + qs@6.13.0: + resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} engines: {node: '>=0.6'} queue-lit@1.5.2: @@ -1364,8 +1492,8 @@ packages: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} - raw-body@3.0.0: - resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==} + raw-body@2.5.2: + resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} engines: {node: '>= 0.8'} readable-stream@3.6.2: @@ -1394,10 +1522,6 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - router@2.2.0: - resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} - engines: {node: '>= 18'} - run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -1416,16 +1540,16 @@ packages: engines: {node: '>=10'} hasBin: true - send@1.2.0: - resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} - engines: {node: '>= 18'} + send@0.19.0: + resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} + engines: {node: '>= 0.8.0'} sentence-case@2.1.1: resolution: {integrity: sha512-ENl7cYHaK/Ktwk5OTD+aDbQ3uC8IByu/6Bkg+HDv8Mm+XnBnppVNalcfJTNsp1ibstKh030/JKQQWglDvtKwEQ==} - serve-static@2.2.0: - resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} - engines: {node: '>= 18'} + serve-static@1.16.2: + resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} + engines: {node: '>= 0.8.0'} set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} @@ -1493,10 +1617,6 @@ packages: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} - statuses@2.0.2: - resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} - engines: {node: '>= 0.8'} - string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -1583,8 +1703,8 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} - type-is@2.0.1: - resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} typescript-eslint@8.36.0: @@ -1628,6 +1748,18 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + + uuid@11.1.0: + resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} + hasBin: true + + validator@13.12.0: + resolution: {integrity: sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==} + engines: {node: '>= 0.10'} + validator@13.15.15: resolution: {integrity: sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==} engines: {node: '>= 0.10'} @@ -1671,9 +1803,6 @@ packages: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} - wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - ws@8.18.3: resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} engines: {node: '>=10.0.0'} @@ -1941,25 +2070,33 @@ snapshots: '@types/estree@1.0.8': {} - '@types/express-serve-static-core@5.0.7': + '@types/express-serve-static-core@4.19.6': dependencies: '@types/node': 24.0.12 '@types/qs': 6.14.0 '@types/range-parser': 1.2.7 '@types/send': 0.17.5 - '@types/express@5.0.3': + '@types/express@4.17.23': dependencies: '@types/body-parser': 1.19.6 - '@types/express-serve-static-core': 5.0.7 + '@types/express-serve-static-core': 4.19.6 + '@types/qs': 6.14.0 '@types/serve-static': 1.15.8 '@types/http-errors@2.0.5': {} '@types/json-schema@7.0.15': {} + '@types/jsonwebtoken@9.0.10': + dependencies: + '@types/ms': 2.1.0 + '@types/node': 24.0.12 + '@types/mime@1.3.5': {} + '@types/ms@2.1.0': {} + '@types/node@24.0.12': dependencies: undici-types: 7.8.0 @@ -1987,6 +2124,8 @@ snapshots: '@types/triple-beam@1.3.5': {} + '@types/uuid@10.0.0': {} + '@types/validator@13.15.2': {} '@types/whois-json@2.0.4': {} @@ -2089,10 +2228,10 @@ snapshots: '@vladfrangu/async_event_emitter@2.4.6': {} - accepts@2.0.0: + accepts@1.3.8: dependencies: - mime-types: 3.0.1 - negotiator: 1.0.0 + mime-types: 2.1.35 + negotiator: 0.6.3 acorn-jsx@5.3.2(acorn@8.15.0): dependencies: @@ -2120,25 +2259,40 @@ snapshots: argparse@2.0.1: {} + array-flatten@1.1.1: {} + array-union@2.1.0: {} async@3.2.6: {} + asynckit@0.4.0: {} + + axios@1.10.0: + dependencies: + follow-redirects: 1.15.9 + form-data: 4.0.4 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + balanced-match@1.0.2: {} binary-extensions@2.3.0: {} - body-parser@2.2.0: + body-parser@1.20.3: dependencies: bytes: 3.1.2 content-type: 1.0.5 - debug: 4.4.1(supports-color@5.5.0) + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 http-errors: 2.0.0 - iconv-lite: 0.6.3 + iconv-lite: 0.4.24 on-finished: 2.4.1 - qs: 6.14.0 - raw-body: 3.0.0 - type-is: 2.0.1 + qs: 6.13.0 + raw-body: 2.5.2 + type-is: 1.6.18 + unpipe: 1.0.0 transitivePeerDependencies: - supports-color @@ -2155,6 +2309,8 @@ snapshots: dependencies: fill-range: 7.1.1 + buffer-equal-constant-time@1.0.1: {} + bytes@3.1.2: {} call-bind-apply-helpers@1.0.2: @@ -2251,6 +2407,10 @@ snapshots: color: 3.2.1 text-hex: 1.0.0 + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + commander@9.5.0: {} concat-map@0.0.1: {} @@ -2260,15 +2420,15 @@ snapshots: snake-case: 2.1.0 upper-case: 1.1.3 - content-disposition@1.0.0: + content-disposition@0.5.4: dependencies: safe-buffer: 5.2.1 content-type@1.0.5: {} - cookie-signature@1.2.2: {} + cookie-signature@1.0.6: {} - cookie@0.7.2: {} + cookie@0.7.1: {} cors@2.8.5: dependencies: @@ -2283,6 +2443,10 @@ snapshots: data-uri-to-buffer@4.0.1: {} + debug@2.6.9: + dependencies: + ms: 2.0.0 + debug@4.4.1(supports-color@5.5.0): dependencies: ms: 2.1.3 @@ -2295,8 +2459,12 @@ snapshots: deep-is@0.1.4: {} + delayed-stream@1.0.0: {} + depd@2.0.0: {} + destroy@1.2.0: {} + dir-glob@3.0.1: dependencies: path-type: 4.0.0 @@ -2334,12 +2502,18 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + ee-first@1.1.1: {} emoji-regex@8.0.0: {} enabled@2.0.0: {} + encodeurl@1.0.2: {} + encodeurl@2.0.0: {} es-define-property@1.0.1: {} @@ -2350,6 +2524,13 @@ snapshots: dependencies: es-errors: 1.3.0 + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + esbuild@0.25.6: optionalDependencies: '@esbuild/aix-ppc64': 0.25.6 @@ -2465,38 +2646,47 @@ snapshots: etag@1.8.1: {} - express-rate-limit@7.5.1(express@5.1.0): + express-rate-limit@7.5.1(express@4.21.2): dependencies: - express: 5.1.0 + express: 4.21.2 - express@5.1.0: + express-validator@7.2.1: dependencies: - accepts: 2.0.0 - body-parser: 2.2.0 - content-disposition: 1.0.0 + lodash: 4.17.21 + validator: 13.12.0 + + express@4.21.2: + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.3 + content-disposition: 0.5.4 content-type: 1.0.5 - cookie: 0.7.2 - cookie-signature: 1.2.2 - debug: 4.4.1(supports-color@5.5.0) + cookie: 0.7.1 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 - finalhandler: 2.1.0 - fresh: 2.0.0 + finalhandler: 1.3.1 + fresh: 0.5.2 http-errors: 2.0.0 - merge-descriptors: 2.0.0 - mime-types: 3.0.1 + merge-descriptors: 1.0.3 + methods: 1.1.2 on-finished: 2.4.1 - once: 1.4.0 parseurl: 1.3.3 + path-to-regexp: 0.1.12 proxy-addr: 2.0.7 - qs: 6.14.0 + qs: 6.13.0 range-parser: 1.2.1 - router: 2.2.0 - send: 1.2.0 - serve-static: 2.2.0 - statuses: 2.0.2 - type-is: 2.0.1 + safe-buffer: 5.2.1 + send: 0.19.0 + serve-static: 1.16.2 + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: 1.6.18 + utils-merge: 1.0.1 vary: 1.1.2 transitivePeerDependencies: - supports-color @@ -2536,14 +2726,15 @@ snapshots: dependencies: to-regex-range: 5.0.1 - finalhandler@2.1.0: + finalhandler@1.3.1: dependencies: - debug: 4.4.1(supports-color@5.5.0) + debug: 2.6.9 encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 parseurl: 1.3.3 - statuses: 2.0.2 + statuses: 2.0.1 + unpipe: 1.0.0 transitivePeerDependencies: - supports-color @@ -2566,13 +2757,23 @@ snapshots: fn.name@1.1.0: {} + follow-redirects@1.15.9: {} + + form-data@4.0.4: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + formdata-polyfill@4.0.10: dependencies: fetch-blob: 3.2.0 forwarded@0.2.0: {} - fresh@2.0.0: {} + fresh@0.5.2: {} fsevents@2.3.3: optional: true @@ -2634,6 +2835,10 @@ snapshots: has-symbols@1.1.0: {} + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + hasown@2.0.2: dependencies: function-bind: 1.1.2 @@ -2655,7 +2860,7 @@ snapshots: statuses: 2.0.1 toidentifier: 1.0.1 - iconv-lite@0.6.3: + iconv-lite@0.4.24: dependencies: safer-buffer: 2.1.2 @@ -2701,8 +2906,6 @@ snapshots: is-number@7.0.0: {} - is-promise@4.0.0: {} - is-stream@2.0.1: {} is-upper-case@1.1.2: @@ -2725,6 +2928,30 @@ snapshots: json5@2.2.3: {} + jsonwebtoken@9.0.2: + dependencies: + jws: 3.2.2 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.7.2 + + jwa@1.4.2: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@3.2.2: + dependencies: + jwa: 1.4.2 + safe-buffer: 5.2.1 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -2744,8 +2971,22 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash.includes@4.3.0: {} + + lodash.isboolean@3.0.3: {} + + lodash.isinteger@4.0.4: {} + + lodash.isnumber@3.0.3: {} + + lodash.isplainobject@4.0.6: {} + + lodash.isstring@4.0.1: {} + lodash.merge@4.6.2: {} + lodash.once@4.1.1: {} + lodash.snakecase@4.1.1: {} lodash@4.17.21: {} @@ -2769,22 +3010,26 @@ snapshots: math-intrinsics@1.1.0: {} - media-typer@1.1.0: {} + media-typer@0.3.0: {} - merge-descriptors@2.0.0: {} + merge-descriptors@1.0.3: {} merge2@1.4.1: {} + methods@1.1.2: {} + micromatch@4.0.8: dependencies: braces: 3.0.3 picomatch: 2.3.1 - mime-db@1.54.0: {} + mime-db@1.52.0: {} - mime-types@3.0.1: + mime-types@2.1.35: dependencies: - mime-db: 1.54.0 + mime-db: 1.52.0 + + mime@1.6.0: {} minimatch@3.1.2: dependencies: @@ -2802,13 +3047,15 @@ snapshots: moment@2.30.1: {} + ms@2.0.0: {} + ms@2.1.3: {} mylas@2.1.13: {} natural-compare@1.4.0: {} - negotiator@1.0.0: {} + negotiator@0.6.3: {} no-case@2.3.2: dependencies: @@ -2845,10 +3092,6 @@ snapshots: dependencies: ee-first: 1.1.1 - once@1.4.0: - dependencies: - wrappy: 1.0.2 - one-time@1.0.0: dependencies: fn.name: 1.1.0 @@ -2903,7 +3146,7 @@ snapshots: path-key@3.1.1: {} - path-to-regexp@8.2.0: {} + path-to-regexp@0.1.12: {} path-type@4.0.0: {} @@ -2971,11 +3214,13 @@ snapshots: forwarded: 0.2.0 ipaddr.js: 1.9.1 + proxy-from-env@1.1.0: {} + pstree.remy@1.1.8: {} punycode@2.3.1: {} - qs@6.14.0: + qs@6.13.0: dependencies: side-channel: 1.1.0 @@ -2985,11 +3230,11 @@ snapshots: range-parser@1.2.1: {} - raw-body@3.0.0: + raw-body@2.5.2: dependencies: bytes: 3.1.2 http-errors: 2.0.0 - iconv-lite: 0.6.3 + iconv-lite: 0.4.24 unpipe: 1.0.0 readable-stream@3.6.2: @@ -3012,16 +3257,6 @@ snapshots: reusify@1.1.0: {} - router@2.2.0: - dependencies: - debug: 4.4.1(supports-color@5.5.0) - depd: 2.0.0 - is-promise: 4.0.0 - parseurl: 1.3.3 - path-to-regexp: 8.2.0 - transitivePeerDependencies: - - supports-color - run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -3034,19 +3269,21 @@ snapshots: semver@7.7.2: {} - send@1.2.0: + send@0.19.0: dependencies: - debug: 4.4.1(supports-color@5.5.0) - encodeurl: 2.0.0 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 escape-html: 1.0.3 etag: 1.8.1 - fresh: 2.0.0 + fresh: 0.5.2 http-errors: 2.0.0 - mime-types: 3.0.1 + mime: 1.6.0 ms: 2.1.3 on-finished: 2.4.1 range-parser: 1.2.1 - statuses: 2.0.2 + statuses: 2.0.1 transitivePeerDependencies: - supports-color @@ -3055,12 +3292,12 @@ snapshots: no-case: 2.3.2 upper-case-first: 1.1.2 - serve-static@2.2.0: + serve-static@1.16.2: dependencies: encodeurl: 2.0.0 escape-html: 1.0.3 parseurl: 1.3.3 - send: 1.2.0 + send: 0.19.0 transitivePeerDependencies: - supports-color @@ -3131,8 +3368,6 @@ snapshots: statuses@2.0.1: {} - statuses@2.0.2: {} - string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -3220,11 +3455,10 @@ snapshots: dependencies: prelude-ls: 1.2.1 - type-is@2.0.1: + type-is@1.6.18: dependencies: - content-type: 1.0.5 - media-typer: 1.1.0 - mime-types: 3.0.1 + media-typer: 0.3.0 + mime-types: 2.1.35 typescript-eslint@8.36.0(eslint@9.30.1)(typescript@5.8.3): dependencies: @@ -3260,6 +3494,12 @@ snapshots: util-deprecate@1.0.2: {} + utils-merge@1.0.1: {} + + uuid@11.1.0: {} + + validator@13.12.0: {} + validator@13.15.15: {} vary@1.1.2: {} @@ -3314,8 +3554,6 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 - wrappy@1.0.2: {} - ws@8.18.3: {} xtend@4.0.2: {} diff --git a/src/commands/fun/joke.ts b/src/commands/fun/joke.ts index 1ff8456..294312f 100644 --- a/src/commands/fun/joke.ts +++ b/src/commands/fun/joke.ts @@ -140,7 +140,9 @@ export default { ); embed.setFooter({ text: 'Ba dum tss! 🄁' }); await interaction.editReply({ embeds: [embed] }); - } catch {} + } catch (error) { + console.error('Error showing punchline:', error); + } }, 3000); } catch (error) { await errorHandler({ diff --git a/src/commands/utilities/ai.ts b/src/commands/utilities/ai.ts index 4c43693..d8f1c82 100644 --- a/src/commands/utilities/ai.ts +++ b/src/commands/utilities/ai.ts @@ -482,7 +482,7 @@ export default { if (reset) { userConversations.delete(userId); await interaction.reply({ - content: await client.getLocaleText('commands.reset', interaction.locale), + content: await client.getLocaleText('commands.ai.reset', interaction.locale), ephemeral: true, }); pendingRequests.delete(userId); diff --git a/src/commands/utilities/cobalt.ts b/src/commands/utilities/cobalt.ts new file mode 100644 index 0000000..de7b132 --- /dev/null +++ b/src/commands/utilities/cobalt.ts @@ -0,0 +1,298 @@ +import { + SlashCommandBuilder, + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + ChatInputCommandInteraction, + ApplicationIntegrationType, + InteractionContextType, +} from 'discord.js'; +import axios from 'axios'; +import { SlashCommandProps } from '../../types/command'; +import BotClient from '../../services/Client'; +import { createCommandLogger } from '../../utils/commandLogger'; +import { createErrorHandler } from '../../utils/errorHandler'; + +interface CobaltResponse { + status: string; + url?: string; + filename?: string; + text?: string; + error?: { + code: string; + message: string; + }; +} + +const commandLogger = createCommandLogger('cobalt'); +const errorHandler = createErrorHandler('cobalt'); + +export default { + data: new SlashCommandBuilder() + .setName('cobalt') + .setNameLocalizations({ + 'es-ES': 'cobalt', + 'es-419': 'cobalt', + 'en-US': 'cobalt', + }) + .setDescription('Download a video or audio from a given URL') + .setDescriptionLocalizations({ + 'es-ES': 'Descarga un video o audio desde una URL', + 'es-419': 'Descarga un video o audio desde una URL', + 'en-US': 'Download a video or audio from a given URL', + }) + .addStringOption((option) => + option + .setName('url') + .setNameLocalizations({ + 'es-ES': 'url', + 'es-419': 'url', + 'en-US': 'url', + }) + .setDescription('The URL of the video to download') + .setDescriptionLocalizations({ + 'es-ES': 'La URL del video a descargar', + 'es-419': 'La URL del video a descargar', + 'en-US': 'The URL of the video to download', + }) + .setRequired(true) + ) + .addStringOption((option) => + option + .setName('video-quality') + .setNameLocalizations({ + 'es-ES': 'calidad-video', + 'es-419': 'calidad-video', + 'en-US': 'video-quality', + }) + .setDescription('The video quality to download') + .setDescriptionLocalizations({ + 'es-ES': 'La calidad de video a descargar', + 'es-419': 'La calidad de video a descargar', + 'en-US': 'The video quality to download', + }) + .addChoices( + { name: '144p', value: '144' }, + { name: '240p', value: '240' }, + { name: '360p', value: '360' }, + { name: '480p', value: '480' }, + { name: '720p', value: '720' }, + { name: '1440p', value: '1440' }, + { name: '2160p (4K)', value: '2160' }, + { name: '4320p (8K)', value: '4320' }, + { name: 'Max', value: 'max' } + ) + ) + .addBooleanOption((option) => + option + .setName('audio-only') + .setNameLocalizations({ + 'es-ES': 'solo-audio', + 'es-419': 'solo-audio', + 'en-US': 'audio-only', + }) + .setDescription('Download audio only') + .setDescriptionLocalizations({ + 'es-ES': 'Descargar solo audio', + 'es-419': 'Descargar solo audio', + 'en-US': 'Download audio only', + }) + ) + .addBooleanOption((option) => + option + .setName('mute-audio') + .setNameLocalizations({ + 'es-ES': 'silenciar-audio', + 'es-419': 'silenciar-audio', + 'en-US': 'mute-audio', + }) + .setDescription('Mute audio') + .setDescriptionLocalizations({ + 'es-ES': 'Silenciar audio', + 'es-419': 'Silenciar audio', + 'en-US': 'Mute audio', + }) + ) + .addBooleanOption((option) => + option + .setName('twitter-gif') + .setNameLocalizations({ + 'es-ES': 'gif-twitter', + 'es-419': 'gif-twitter', + 'en-US': 'twitter-gif', + }) + .setDescription('Download as Twitter GIF') + .setDescriptionLocalizations({ + 'es-ES': 'Descargar como GIF de Twitter', + 'es-419': 'Descargar como GIF de Twitter', + 'en-US': 'Download as Twitter GIF', + }) + ) + .addBooleanOption((option) => + option + .setName('tiktok-original-audio') + .setNameLocalizations({ + 'es-ES': 'audio-original-tiktok', + 'es-419': 'audio-original-tiktok', + 'en-US': 'tiktok-original-audio', + }) + .setDescription('Include TikTok original audio') + .setDescriptionLocalizations({ + 'es-ES': 'Incluir audio original de TikTok', + 'es-419': 'Incluir audio original de TikTok', + 'en-US': 'Include TikTok original audio', + }) + ) + .addStringOption((option) => + option + .setName('audio-format') + .setNameLocalizations({ + 'es-ES': 'formato-audio', + 'es-419': 'formato-audio', + 'en-US': 'audio-format', + }) + .setDescription('Format for audio (requires audio-only to be true)') + .setDescriptionLocalizations({ + 'es-ES': 'Formato para audio (requiere solo-audio activado)', + 'es-419': 'Formato para audio (requiere solo-audio activado)', + 'en-US': 'Format for audio (requires audio-only to be true)', + }) + .addChoices( + { name: 'MP3', value: 'mp3' }, + { name: 'OGG', value: 'ogg' }, + { name: 'WAV', value: 'wav' }, + { name: 'Best', value: 'best' } + ) + ) + .setContexts([ + InteractionContextType.BotDM, + InteractionContextType.Guild, + InteractionContextType.PrivateChannel, + ]) + .setIntegrationTypes(ApplicationIntegrationType.UserInstall), + category: 'utilities', + async execute(client: BotClient, interaction: ChatInputCommandInteraction): Promise { + try { + commandLogger.logFromInteraction(interaction); + + const url = interaction.options.getString('url', true); + const videoQuality = interaction.options.getString('video-quality'); + const audioOnly = interaction.options.getBoolean('audio-only'); + const muteAudio = interaction.options.getBoolean('mute-audio'); + const twitterGif = interaction.options.getBoolean('twitter-gif'); + const tiktokOriginalAudio = interaction.options.getBoolean('tiktok-original-audio'); + const audioFormat = interaction.options.getString('audio-format'); + + await interaction.deferReply(); + + const requestBody = { + url: url, + videoQuality: videoQuality || 'max', + audioFormat: audioFormat || 'mp3', + downloadMode: audioOnly ? 'audio' : muteAudio ? 'mute' : 'auto', + filenameStyle: 'basic', + tiktokFullAudio: tiktokOriginalAudio || false, + convertGif: twitterGif || false, + }; + + const response = await axios.post('https://cobalt.aethel.xyz/', requestBody, { + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + }); + + const data: CobaltResponse = response.data; + + if (data.status === 'tunnel' || data.status === 'redirect') { + const downloadUrl = data.url; + if (!downloadUrl) { + throw new Error('No download URL received'); + } + + const buttonLabel = await client.getLocaleText( + 'commands.cobalt.button_label', + interaction.locale + ); + const successMessage = await client.getLocaleText( + 'commands.cobalt.success', + interaction.locale, + { + url, + filename: data.filename || 'download', + } + ); + + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder().setLabel(buttonLabel).setStyle(ButtonStyle.Link).setURL(downloadUrl) + ); + + await interaction.editReply({ + content: successMessage, + components: [row], + }); + } else if (data.status === 'error') { + const unknownError = await client.getLocaleText( + 'commands.cobalt.unknown_error', + interaction.locale + ); + let errorText = unknownError; + + if (data.error) { + errorText = data.error.message || data.error.code || unknownError; + } else if (data.text) { + errorText = data.text; + } + + const errorMessage = await client.getLocaleText( + 'commands.cobalt.error', + interaction.locale, + { + error: errorText, + } + ); + + await interaction.editReply({ + content: errorMessage, + }); + } else if (data.status === 'picker') { + const multipleItemsMessage = await client.getLocaleText( + 'commands.cobalt.multiple_items', + interaction.locale, + { url } + ); + + await interaction.editReply({ + content: + multipleItemsMessage || 'Multiple items found. Please provide a more specific URL.', + }); + } else if (data.status === 'local-processing') { + const notSupportedMessage = await client.getLocaleText( + 'commands.cobalt.local_processing_not_supported', + interaction.locale + ); + + await interaction.editReply({ + content: + notSupportedMessage || + 'Local processing is not supported. Please try a different URL or option.', + }); + } else { + const unknownResponseMessage = await client.getLocaleText( + 'commands.cobalt.unknown_response', + interaction.locale + ); + + await interaction.editReply({ + content: unknownResponseMessage, + }); + } + } catch (error) { + await errorHandler({ + interaction, + client, + error: error as Error, + }); + } + }, +} as SlashCommandProps; diff --git a/src/commands/utilities/remind.ts b/src/commands/utilities/remind.ts index b5540f8..eac5916 100644 --- a/src/commands/utilities/remind.ts +++ b/src/commands/utilities/remind.ts @@ -24,6 +24,7 @@ import { completeReminder, cleanupReminders, ensureUserRegistered, + getActiveReminders, DatabaseError, } from '@/utils/reminderDb'; import { RemindCommandProps } from '@/types/command'; @@ -70,6 +71,84 @@ const activeReminders = new Map(); const commandLogger = createCommandLogger('remind'); const errorHandler = createErrorHandler('remind'); +export async function loadActiveReminders(client: BotClient) { + try { + const reminders = await getActiveReminders(); + logger.info(`Loading ${reminders.length} active reminders from database`); + + for (const reminder of reminders) { + const now = Date.now(); + const expiresAt = new Date(reminder.expires_at).getTime(); + const timeUntilExpiry = expiresAt - now; + + if (timeUntilExpiry <= 0) { + logger.warn(`Skipping expired reminder ${reminder.reminder_id}`); + continue; + } + + const timeoutId = setTimeout( + createReminderHandler(client, { + ...reminder, + created_at: reminder.created_at || new Date(reminder.expires_at), + }), + timeUntilExpiry + ); + + activeReminders.set(reminder.reminder_id, { + timeoutId, + expiresAt, + }); + + logger.debug( + `Scheduled reminder ${reminder.reminder_id} for ${new Date(expiresAt).toISOString()}` + ); + } + + logger.info(`Successfully loaded ${activeReminders.size} active reminders`); + } catch (error) { + logger.error('Error loading active reminders:', error); + } +} + +export function scheduleReminder(client: BotClient, reminder: Reminder) { + try { + const now = Date.now(); + const expiresAt = new Date(reminder.expires_at).getTime(); + const timeUntilExpiry = expiresAt - now; + + if (timeUntilExpiry <= 0) { + logger.warn(`Cannot schedule expired reminder ${reminder.reminder_id}`); + return false; + } + + const existingReminder = activeReminders.get(reminder.reminder_id); + if (existingReminder) { + clearTimeout(existingReminder.timeoutId); + } + + const timeoutId = setTimeout( + createReminderHandler(client, { + ...reminder, + created_at: reminder.created_at || new Date(), + }), + timeUntilExpiry + ); + + activeReminders.set(reminder.reminder_id, { + timeoutId, + expiresAt, + }); + + logger.info( + `Scheduled reminder ${reminder.reminder_id} for ${new Date(expiresAt).toISOString()}` + ); + return true; + } catch (error) { + logger.error(`Error scheduling reminder ${reminder.reminder_id}:`, error); + return false; + } +} + declare global { var _reminders: Map; } diff --git a/src/events/ready.ts b/src/events/ready.ts index 6993d52..09b939c 100644 --- a/src/events/ready.ts +++ b/src/events/ready.ts @@ -1,5 +1,6 @@ import BotClient from '@/services/Client'; import logger from '@/utils/logger'; +import { loadActiveReminders } from '@/commands/utilities/remind'; export default class ReadyEvent { constructor(c: BotClient) { @@ -10,6 +11,8 @@ export default class ReadyEvent { try { logger.info(`Logged in as ${client.user?.username}`); await client.application?.commands.fetch({ withLocalizations: true }); + + await loadActiveReminders(client); } catch (error) { logger.error('Error during ready event:', error); } diff --git a/src/index.ts b/src/index.ts index a7c2364..2872fcc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,6 +8,10 @@ import { ALLOWED_ORIGINS, PORT, RATE_LIMIT_WINDOW_MS, RATE_LIMIT_MAX } from './c import rateLimit from 'express-rate-limit'; import authenticateApiKey from './middlewares/verifyApiKey'; import status from './routes/status'; +import authRoutes from './routes/auth'; +import todosRoutes from './routes/todos'; +import apiKeysRoutes from './routes/apiKeys'; +import remindersRoutes from './routes/reminders'; import { resetOldStrikes } from './utils/userStrikes'; config(); @@ -26,9 +30,12 @@ const app = e(); app.use(helmet()); app.use( cors({ - origin: ALLOWED_ORIGINS ? ALLOWED_ORIGINS.split(',') : '*', - methods: ['GET'], - allowedHeaders: ['Content-Type', 'X-API-Key'], + origin: ALLOWED_ORIGINS + ? ALLOWED_ORIGINS.split(',') + : ['http://localhost:3000', 'http://localhost:8080'], + methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], + allowedHeaders: ['Content-Type', 'X-API-Key', 'Authorization'], + credentials: true, maxAge: 86400, }) ); @@ -43,22 +50,45 @@ app.use( }) ); +app.use(e.json({ limit: '10mb' })); +app.use(e.urlencoded({ extended: true, limit: '10mb' })); + app.use((req, res, next) => { res.setHeader('X-Content-Type-Options', 'nosniff'); res.setHeader('X-Frame-Options', 'DENY'); - res.setHeader('Content-Security-Policy', "default-src 'none'"); - res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate'); - res.setHeader('Pragma', 'no-cache'); - res.setHeader('Expires', '0'); - res.setHeader('Surrogate-Control', 'no-store'); + if (req.path.startsWith('/api/') || req.path.startsWith('/status')) { + res.setHeader('Content-Security-Policy', "default-src 'none'"); + res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate'); + res.setHeader('Pragma', 'no-cache'); + res.setHeader('Expires', '0'); + res.setHeader('Surrogate-Control', 'no-store'); + } else { + res.setHeader( + 'Content-Security-Policy', + "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https:" + ); + } next(); }); const bot = new BotClient(); bot.init(); -app.use(authenticateApiKey); -app.use('/status', status(bot)); +app.use('/api/auth', authRoutes); +app.use('/api/todos', todosRoutes); +app.use('/api/user/api-keys', apiKeysRoutes); +app.use('/api/reminders', remindersRoutes); + +app.use('/status', authenticateApiKey, status(bot)); + +app.use(e.static('web/dist')); + +app.get('*', (req, res) => { + if (req.path.startsWith('/api/') || req.path.startsWith('/status')) { + return res.status(404).json({ error: 'Not found' }); + } + res.sendFile('index.html', { root: 'web/dist' }); +}); setInterval( () => { diff --git a/src/middlewares/auth.ts b/src/middlewares/auth.ts new file mode 100644 index 0000000..5a94e84 --- /dev/null +++ b/src/middlewares/auth.ts @@ -0,0 +1,58 @@ +import { Request, Response, NextFunction } from 'express'; +import jwt from 'jsonwebtoken'; +import logger from '../utils/logger'; + +const JWT_SECRET = process.env.JWT_SECRET || 'your-jwt-secret'; + +interface JwtPayload { + userId: string; + username: string; + discriminator: string; + avatar?: string; + iat?: number; + exp?: number; +} + +export const authenticateToken = (req: Request, res: Response, next: NextFunction) => { + const authHeader = req.headers['authorization']; + const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN + + if (!token) { + return res.status(401).json({ error: 'Access token required' }); + } + + try { + const decoded = jwt.verify(token, JWT_SECRET) as JwtPayload; + req.user = decoded; + next(); + } catch (error) { + if (error instanceof jwt.TokenExpiredError) { + logger.debug('Expired JWT token used'); + return res.status(401).json({ error: 'Token expired' }); + } else if (error instanceof jwt.JsonWebTokenError) { + logger.debug('Invalid JWT token used'); + return res.status(401).json({ error: 'Invalid token' }); + } else { + logger.error('JWT verification error:', error); + return res.status(500).json({ error: 'Token verification failed' }); + } + } +}; + +export const optionalAuth = (req: Request, res: Response, next: NextFunction) => { + const authHeader = req.headers['authorization']; + const token = authHeader && authHeader.split(' ')[1]; + + if (!token) { + return next(); + } + + try { + const decoded = jwt.verify(token, JWT_SECRET) as JwtPayload; + req.user = decoded; + } catch (error) { + logger.debug('Optional auth token verification failed:', error); + } + + next(); +}; diff --git a/src/routes/apiKeys.ts b/src/routes/apiKeys.ts new file mode 100644 index 0000000..a9b8722 --- /dev/null +++ b/src/routes/apiKeys.ts @@ -0,0 +1,268 @@ +import { Router } from 'express'; +import pool from '../utils/pgClient'; +import logger from '../utils/logger'; +import { authenticateToken } from '../middlewares/auth'; +import { body, validationResult } from 'express-validator'; +import { encrypt as encryptApiKey } from '../utils/encrypt'; + +const router = Router(); + +router.use(authenticateToken); + +router.get('/', async (req, res) => { + try { + const userId = req.user?.userId; + if (!userId) { + return res.status(401).json({ error: 'User not authenticated' }); + } + + const query = ` + SELECT custom_model, custom_api_url, + CASE WHEN api_key_encrypted IS NOT NULL THEN TRUE ELSE FALSE END as has_api_key + FROM users + WHERE user_id = $1 + `; + const result = await pool.query(query, [userId]); + + if (result.rows.length === 0) { + return res.status(404).json({ error: 'User not found' }); + } + + const user = result.rows[0]; + res.json({ + hasApiKey: user.has_api_key, + model: user.custom_model, + apiUrl: user.custom_api_url, + }); + } catch (error) { + logger.error('Error fetching API key info:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +router.post( + '/', + body('apiKey') + .trim() + .isLength({ min: 1, max: 1000 }) + .withMessage('API key is required and must be less than 1000 characters'), + body('model') + .optional() + .trim() + .isLength({ max: 100 }) + .withMessage('Model name must be less than 100 characters'), + body('apiUrl') + .optional() + .trim() + .isURL({ require_protocol: true }) + .withMessage('API URL must be a valid URL with protocol'), + async (req, res) => { + try { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ error: errors.array()[0].msg }); + } + + const userId = req.user?.userId; + if (!userId) { + return res.status(401).json({ error: 'User not authenticated' }); + } + + const { apiKey, model, apiUrl } = req.body; + + const encryptedApiKey = encryptApiKey(apiKey); + + const query = ` + UPDATE users + SET api_key_encrypted = $1, + custom_model = $2, + custom_api_url = $3 + WHERE user_id = $4 + RETURNING user_id + `; + const result = await pool.query(query, [ + encryptedApiKey, + model || null, + apiUrl || null, + userId, + ]); + + if (result.rows.length === 0) { + return res.status(404).json({ error: 'User not found' }); + } + + logger.info(`API key updated for user ${userId}`); + res.json({ message: 'API key updated successfully' }); + } catch (error) { + logger.error('Error updating API key:', error); + res.status(500).json({ error: 'Internal server error' }); + } + } +); + +router.put( + '/', + body('apiKey') + .trim() + .isLength({ min: 1, max: 1000 }) + .withMessage('API key is required and must be less than 1000 characters'), + body('model') + .optional() + .trim() + .isLength({ max: 100 }) + .withMessage('Model name must be less than 100 characters'), + body('apiUrl') + .optional() + .trim() + .isURL({ require_protocol: true }) + .withMessage('API URL must be a valid URL with protocol'), + async (req, res) => { + try { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ error: errors.array()[0].msg }); + } + + const userId = req.user?.userId; + if (!userId) { + return res.status(401).json({ error: 'User not authenticated' }); + } + + const { apiKey, model, apiUrl } = req.body; + + const encryptedApiKey = encryptApiKey(apiKey); + + const query = ` + UPDATE users + SET api_key_encrypted = $1, + custom_model = $2, + custom_api_url = $3 + WHERE user_id = $4 + RETURNING user_id + `; + const result = await pool.query(query, [ + encryptedApiKey, + model || null, + apiUrl || null, + userId, + ]); + + if (result.rows.length === 0) { + return res.status(404).json({ error: 'User not found' }); + } + + logger.info(`API key updated for user ${userId}`); + res.json({ message: 'API key updated successfully' }); + } catch (error) { + logger.error('Error updating API key:', error); + res.status(500).json({ error: 'Internal server error' }); + } + } +); + +router.delete('/', async (req, res) => { + try { + const userId = req.user?.userId; + if (!userId) { + return res.status(401).json({ error: 'User not authenticated' }); + } + + const query = ` + UPDATE users + SET api_key_encrypted = NULL, + custom_model = NULL, + custom_api_url = NULL + WHERE user_id = $1 + RETURNING user_id + `; + const result = await pool.query(query, [userId]); + + if (result.rows.length === 0) { + return res.status(404).json({ error: 'User not found' }); + } + + logger.info(`API key deleted for user ${userId}`); + res.json({ message: 'API key deleted successfully' }); + } catch (error) { + logger.error('Error deleting API key:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +router.post( + '/test', + body('apiKey').trim().isLength({ min: 1 }).withMessage('API key is required'), + body('model').optional().trim(), + body('apiUrl') + .optional() + .trim() + .isURL({ require_protocol: true }) + .withMessage('API URL must be a valid URL with protocol'), + async (req, res) => { + try { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ error: errors.array()[0].msg }); + } + + const { apiKey, model, apiUrl } = req.body; + const userId = req.user?.userId; + + const fullApiUrl = apiUrl || 'https://api.openai.com/v1/chat/completions'; + const testModel = model || 'gpt-3.5-turbo'; + + const testResponse = await fetch(fullApiUrl, { + method: 'POST', + headers: { + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + model: testModel, + messages: [ + { + role: 'user', + content: + 'Hello! This is a test message. Please respond with "API key test successful!"', + }, + ], + max_tokens: 50, + temperature: 0.1, + }), + }); + + if (!testResponse.ok) { + const errorData = await testResponse.json().catch(() => ({})); + const errorMessage = + errorData.error?.message || `HTTP ${testResponse.status}: ${testResponse.statusText}`; + + logger.warn(`API key test failed for user ${userId}: ${errorMessage}`); + return res.status(400).json({ + error: `API key test failed: ${errorMessage}`, + }); + } + + const responseData = await testResponse.json(); + const testMessage = responseData.choices?.[0]?.message?.content || 'Test completed'; + + logger.info(`API key test successful for user ${userId}`); + res.json({ + success: true, + message: 'API key test successful!', + testResponse: testMessage.substring(0, 100), + }); + } catch (error) { + logger.error('Error testing API key:', error); + + if (error instanceof TypeError && error.message.includes('fetch')) { + return res + .status(400) + .json({ error: 'Failed to connect to API endpoint. Please check the URL.' }); + } + + res.status(500).json({ error: 'API key test failed due to server error' }); + } + } +); + +export default router; diff --git a/src/routes/auth.ts b/src/routes/auth.ts new file mode 100644 index 0000000..2f2b9cd --- /dev/null +++ b/src/routes/auth.ts @@ -0,0 +1,151 @@ +import { Router } from 'express'; +import jwt from 'jsonwebtoken'; +import pool from '../utils/pgClient'; +import logger from '../utils/logger'; +import { authenticateToken } from '../middlewares/auth'; + +const router = Router(); + +const DISCORD_CLIENT_ID = process.env.DISCORD_CLIENT_ID; +const DISCORD_CLIENT_SECRET = process.env.DISCORD_CLIENT_SECRET; +const DISCORD_REDIRECT_URI = + process.env.DISCORD_REDIRECT_URI || 'http://localhost:8080/api/auth/discord/callback'; +const JWT_SECRET = process.env.JWT_SECRET || 'your-jwt-secret'; +const FRONTEND_URL = process.env.FRONTEND_URL || 'http://localhost:3000'; + +interface DiscordUser { + id: string; + username: string; + discriminator: string; + avatar?: string; + email?: string; +} + +router.get('/discord', (req, res) => { + const discordAuthUrl = `https://discord.com/api/oauth2/authorize?client_id=${DISCORD_CLIENT_ID}&redirect_uri=${encodeURIComponent(DISCORD_REDIRECT_URI)}&response_type=code&scope=identify`; + res.redirect(discordAuthUrl); +}); + +router.get('/discord/callback', async (req, res) => { + const { code, error } = req.query; + + if (error) { + logger.error('Discord OAuth error:', error); + return res.redirect(`${FRONTEND_URL}/login?error=oauth_error`); + } + + if (!code) { + logger.error('No authorization code received from Discord'); + return res.redirect(`${FRONTEND_URL}/login?error=no_code`); + } + + try { + const tokenResponse = await fetch('https://discord.com/api/oauth2/token', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams({ + client_id: DISCORD_CLIENT_ID!, + client_secret: DISCORD_CLIENT_SECRET!, + grant_type: 'authorization_code', + code: code as string, + redirect_uri: DISCORD_REDIRECT_URI, + }), + }); + + if (!tokenResponse.ok) { + throw new Error('Failed to exchange code for token'); + } + + const tokenData = await tokenResponse.json(); + + const userResponse = await fetch('https://discord.com/api/users/@me', { + headers: { + Authorization: `Bearer ${tokenData.access_token}`, + }, + }); + + if (!userResponse.ok) { + throw new Error('Failed to fetch user information'); + } + + const discordUser: DiscordUser = await userResponse.json(); + + const userQuery = 'SELECT user_id, created_at FROM users WHERE user_id = $1'; + const userResult = await pool.query(userQuery, [discordUser.id]); + + if (userResult.rows.length === 0) { + const insertQuery = + 'INSERT INTO users (user_id, language, created_at) VALUES ($1, $2, NOW())'; + await pool.query(insertQuery, [discordUser.id, 'en']); + logger.info( + `New user created: ${discordUser.username}#${discordUser.discriminator} (${discordUser.id})` + ); + } + + const jwtToken = jwt.sign( + { + userId: discordUser.id, + username: discordUser.username, + discriminator: discordUser.discriminator === '0' ? null : discordUser.discriminator, + avatar: discordUser.avatar, + }, + JWT_SECRET, + { expiresIn: '7d' } + ); + + const redirectUrl = new URL(`${FRONTEND_URL}/login`); + redirectUrl.searchParams.set('token', jwtToken); + redirectUrl.searchParams.set('user_id', discordUser.id); + redirectUrl.searchParams.set('username', discordUser.username); + if (discordUser.discriminator && discordUser.discriminator !== '0') { + redirectUrl.searchParams.set('discriminator', discordUser.discriminator); + } + if (discordUser.avatar) { + redirectUrl.searchParams.set('avatar', discordUser.avatar); + } + + res.redirect(redirectUrl.toString()); + } catch (error) { + logger.error('Discord OAuth callback error:', error); + res.redirect(`${FRONTEND_URL}/login?error=auth_failed`); + } +}); + +router.get('/me', authenticateToken, async (req, res) => { + try { + const userId = req.user?.userId; + if (!userId) { + return res.status(401).json({ error: 'User not authenticated' }); + } + + const userQuery = 'SELECT user_id, language, created_at FROM users WHERE user_id = $1'; + const userResult = await pool.query(userQuery, [userId]); + + if (userResult.rows.length === 0) { + return res.status(404).json({ error: 'User not found' }); + } + + const user = userResult.rows[0]; + res.json({ + user: { + id: user.user_id, + username: req.user?.username, + discriminator: req.user?.discriminator, + avatar: req.user?.avatar, + language: user.language, + createdAt: user.created_at, + }, + }); + } catch (error) { + logger.error('Error fetching user info:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +router.post('/logout', authenticateToken, (req, res) => { + res.json({ message: 'Logged out successfully' }); +}); + +export default router; diff --git a/src/routes/reminders.ts b/src/routes/reminders.ts new file mode 100644 index 0000000..2bd2708 --- /dev/null +++ b/src/routes/reminders.ts @@ -0,0 +1,273 @@ +import { Router, Request, Response } from 'express'; +import { v4 as uuidv4 } from 'uuid'; +import { + saveReminder, + getUserReminders, + getReminder, + completeReminder, + getActiveReminders, + clearCompletedReminders, +} from '../utils/reminderDb'; +import { authenticateToken } from '../middlewares/auth'; +import logger from '../utils/logger'; +import BotClient from '../services/Client'; +import { EmbedBuilder } from 'discord.js'; +import { formatTimeString } from '../utils/validation'; +import { scheduleReminder } from '../commands/utilities/remind'; + +const router = Router(); + +router.get('/', authenticateToken, async (req: Request, res: Response) => { + try { + const userId = req.user?.userId; + if (!userId) { + return res.status(401).json({ error: 'User not authenticated' }); + } + + const reminders = await getUserReminders(userId); + res.json({ reminders }); + } catch (error) { + logger.error('Error fetching user reminders:', error); + res.status(500).json({ error: 'Failed to fetch reminders' }); + } +}); + +router.post('/', authenticateToken, async (req: Request, res: Response) => { + try { + const userId = req.user?.userId; + const userTag = req.user?.username || 'Unknown'; + + if (!userId) { + return res.status(401).json({ error: 'User not authenticated' }); + } + + const { message, expires_at } = req.body; + + if (!message || !expires_at) { + return res.status(400).json({ error: 'Message and expiration date are required' }); + } + + const reminderData = { + reminder_id: uuidv4(), + user_id: userId, + user_tag: userTag, + channel_id: 'web', + guild_id: null, + message, + expires_at: new Date(expires_at), + locale: 'en', + metadata: { + source: 'web', + created_via: 'dashboard', + }, + }; + + const savedReminder = await saveReminder(reminderData); + + try { + const client = BotClient.getInstance(); + if (client) { + const scheduled = scheduleReminder(client, { + ...savedReminder, + created_at: savedReminder.created_at || new Date(), + }); + + if (scheduled) { + logger.info( + `Successfully scheduled reminder ${savedReminder.reminder_id} from dashboard` + ); + } else { + logger.warn(`Failed to schedule reminder ${savedReminder.reminder_id} from dashboard`); + } + } else { + logger.warn('Bot client not available, reminder saved but not scheduled'); + } + } catch (schedulingError) { + logger.error(`Error scheduling reminder ${savedReminder.reminder_id}:`, schedulingError); + } + + res.status(201).json({ reminder: savedReminder }); + } catch (error) { + logger.error('Error creating reminder:', error); + res.status(500).json({ error: 'Failed to create reminder' }); + } +}); + +router.get('/:id', authenticateToken, async (req: Request, res: Response) => { + try { + const userId = req.user?.userId; + const reminderId = req.params.id; + + if (!userId) { + return res.status(401).json({ error: 'User not authenticated' }); + } + + const reminder = await getReminder(reminderId); + + if (!reminder) { + return res.status(404).json({ error: 'Reminder not found' }); + } + + if (reminder.user_id !== userId) { + return res.status(403).json({ error: 'Access denied' }); + } + + res.json({ reminder }); + } catch (error) { + logger.error('Error fetching reminder:', error); + res.status(500).json({ error: 'Failed to fetch reminder' }); + } +}); + +router.patch('/:id/complete', authenticateToken, async (req: Request, res: Response) => { + try { + const userId = req.user?.userId; + const reminderId = req.params.id; + + if (!userId) { + return res.status(401).json({ error: 'User not authenticated' }); + } + + const reminder = await getReminder(reminderId); + + if (!reminder) { + return res.status(404).json({ error: 'Reminder not found' }); + } + + if (reminder.user_id !== userId) { + return res.status(403).json({ error: 'Access denied' }); + } + + try { + const client = BotClient.getInstance(); + if (client) { + const user = await client.users.fetch(reminder.user_id); + if (user) { + const minutes = Math.floor( + (new Date(reminder.expires_at).getTime() - new Date(reminder.created_at!).getTime()) / + (60 * 1000) + ); + + const reminderTitle = + 'ā° ' + (await client.getLocaleText('commands.remind.reminder', reminder.locale)); + const reminderDesc = await client.getLocaleText( + 'commands.remind.remindyou', + reminder.locale, + { message: reminder.message } + ); + + const timeElapsedText = + 'ā±ļø ' + (await client.getLocaleText('commands.remind.timeelapsed', reminder.locale)); + const originalTimeText = + 'šŸ“… ' + (await client.getLocaleText('commands.remind.originaltime', reminder.locale)); + + const reminderEmbed = new EmbedBuilder() + .setColor(0xfaa0a0) + .setTitle(reminderTitle) + .setDescription(reminderDesc) + .addFields( + { name: timeElapsedText, value: formatTimeString(minutes), inline: true }, + { + name: originalTimeText, + value: ``, + inline: true, + } + ) + .setFooter({ text: `ID: ${reminder.reminder_id.slice(-6)}` }) + .setTimestamp(); + + if (reminder.metadata?.message_url) { + const originalMessageText = await client.getLocaleText( + 'common.ogmessage', + reminder.locale + ); + const jumpToMessageText = await client.getLocaleText( + 'common.jumptomessage', + reminder.locale + ); + + reminderEmbed.addFields({ + name: originalMessageText, + value: `[${jumpToMessageText}](${reminder.metadata.message_url})`, + inline: false, + }); + } + + if (reminder.message.includes('http') && !reminder.metadata?.message_url) { + const messageLinkText = await client.getLocaleText( + 'common.messagelink', + reminder.locale + ); + reminderEmbed.addFields({ + name: messageLinkText, + value: reminder.message, + inline: false, + }); + } + + await user.send({ + embeds: [reminderEmbed], + }); + + logger.info(`Successfully sent reminder to ${reminder.user_tag} (${reminder.user_id})`, { + reminderId: reminder.reminder_id, + }); + } + } + } catch (notificationError) { + logger.error( + `Failed to send reminder notification to ${reminder.user_tag} (${reminder.user_id}): ${(notificationError as Error).message}`, + { + error: notificationError, + reminderId: reminder.reminder_id, + } + ); + } + + const completedReminder = await completeReminder(reminderId); + res.json({ reminder: completedReminder }); + } catch (error) { + logger.error('Error completing reminder:', error); + res.status(500).json({ error: 'Failed to complete reminder' }); + } +}); + +router.get('/active/all', authenticateToken, async (req: Request, res: Response) => { + try { + const userId = req.user?.userId; + + if (!userId) { + return res.status(401).json({ error: 'User not authenticated' }); + } + + const activeReminders = await getActiveReminders(); + const userActiveReminders = activeReminders.filter((reminder) => reminder.user_id === userId); + + res.json({ reminders: userActiveReminders }); + } catch (error) { + logger.error('Error fetching active reminders:', error); + res.status(500).json({ error: 'Failed to fetch active reminders' }); + } +}); + +router.delete('/completed', authenticateToken, async (req: Request, res: Response) => { + try { + const userId = req.user?.userId; + + if (!userId) { + return res.status(401).json({ error: 'User not authenticated' }); + } + + const deletedCount = await clearCompletedReminders(userId); + + res.json({ + message: `Successfully cleared ${deletedCount} completed reminders`, + deletedCount, + }); + } catch (error) { + logger.error('Error clearing completed reminders:', error); + res.status(500).json({ error: 'Failed to clear completed reminders' }); + } +}); + +export default router; diff --git a/src/routes/todos.ts b/src/routes/todos.ts new file mode 100644 index 0000000..3d0a96e --- /dev/null +++ b/src/routes/todos.ts @@ -0,0 +1,190 @@ +import { Router } from 'express'; +import pool from '../utils/pgClient'; +import logger from '../utils/logger'; +import { authenticateToken } from '../middlewares/auth'; +import { body, validationResult } from 'express-validator'; + +const router = Router(); + +router.use(authenticateToken); + +router.get('/', async (req, res) => { + try { + const userId = req.user?.userId; + if (!userId) { + return res.status(401).json({ error: 'User not authenticated' }); + } + + const query = ` + SELECT id, item, done, created_at, completed_at + FROM todos + WHERE user_id = $1 + ORDER BY created_at DESC + `; + const result = await pool.query(query, [userId]); + + res.json(result.rows); + } catch (error) { + logger.error('Error fetching todos:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +router.post( + '/', + body('item') + .trim() + .isLength({ min: 1, max: 500 }) + .withMessage('Todo item must be between 1 and 500 characters'), + async (req, res) => { + try { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ error: errors.array()[0].msg }); + } + + const userId = req.user?.userId; + if (!userId) { + return res.status(401).json({ error: 'User not authenticated' }); + } + + const { item } = req.body; + + const query = ` + INSERT INTO todos (user_id, item, done, created_at) + VALUES ($1, $2, FALSE, NOW()) + RETURNING id, item, done, created_at, completed_at + `; + const result = await pool.query(query, [userId, item]); + + logger.info(`Todo created for user ${userId}: ${item}`); + res.status(201).json(result.rows[0]); + } catch (error) { + logger.error('Error creating todo:', error); + res.status(500).json({ error: 'Internal server error' }); + } + } +); + +router.put( + '/:id', + body('done').isBoolean().withMessage('Done must be a boolean value'), + async (req, res) => { + try { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ error: errors.array()[0].msg }); + } + + const userId = req.user?.userId; + if (!userId) { + return res.status(401).json({ error: 'User not authenticated' }); + } + + const { id } = req.params as { id: string }; + const { done } = req.body; + + const checkQuery = 'SELECT id FROM todos WHERE id = $1 AND user_id = $2'; + const checkResult = await pool.query(checkQuery, [id, userId]); + + if (checkResult.rows.length === 0) { + return res.status(404).json({ error: 'Todo not found' }); + } + + const query = ` + UPDATE todos + SET done = $1, completed_at = CASE WHEN $1 = TRUE THEN NOW() ELSE NULL END + WHERE id = $2 AND user_id = $3 + RETURNING id, item, done, created_at, completed_at + `; + const result = await pool.query(query, [done, id, userId]); + + logger.info(`Todo ${done ? 'completed' : 'uncompleted'} for user ${userId}: ${id}`); + res.json(result.rows[0]); + } catch (error) { + logger.error('Error updating todo:', error); + res.status(500).json({ error: 'Internal server error' }); + } + } +); + +router.delete('/:id', async (req, res) => { + try { + const userId = req.user?.userId; + if (!userId) { + return res.status(401).json({ error: 'User not authenticated' }); + } + + const { id } = req.params as { id: string }; + + const checkQuery = 'SELECT id, item FROM todos WHERE id = $1 AND user_id = $2'; + const checkResult = await pool.query(checkQuery, [id, userId]); + + if (checkResult.rows.length === 0) { + return res.status(404).json({ error: 'Todo not found' }); + } + + const deleteQuery = 'DELETE FROM todos WHERE id = $1 AND user_id = $2'; + await pool.query(deleteQuery, [id, userId]); + + logger.info(`Todo deleted for user ${userId}: ${checkResult.rows[0].item}`); + res.json({ message: 'Todo deleted successfully' }); + } catch (error) { + logger.error('Error deleting todo:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +router.delete('/', async (req, res) => { + try { + const userId = req.user?.userId; + if (!userId) { + return res.status(401).json({ error: 'User not authenticated' }); + } + + const countQuery = 'SELECT COUNT(*) as count FROM todos WHERE user_id = $1'; + const countResult = await pool.query(countQuery, [userId]); + const todoCount = parseInt(countResult.rows[0].count); + + const deleteQuery = 'DELETE FROM todos WHERE user_id = $1'; + await pool.query(deleteQuery, [userId]); + + logger.info(`All todos cleared for user ${userId}: ${todoCount} todos deleted`); + res.json({ message: `${todoCount} todos cleared successfully` }); + } catch (error) { + logger.error('Error clearing todos:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +router.get('/stats', async (req, res) => { + try { + const userId = req.user?.userId; + if (!userId) { + return res.status(401).json({ error: 'User not authenticated' }); + } + + const query = ` + SELECT + COUNT(*) as total, + COUNT(CASE WHEN done = TRUE THEN 1 END) as completed, + COUNT(CASE WHEN done = FALSE THEN 1 END) as pending + FROM todos + WHERE user_id = $1 + `; + const result = await pool.query(query, [userId]); + + const stats = { + total: parseInt(result.rows[0].total), + completed: parseInt(result.rows[0].completed), + pending: parseInt(result.rows[0].pending), + }; + + res.json(stats); + } catch (error) { + logger.error('Error fetching todo stats:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +export default router; diff --git a/src/services/Client.ts b/src/services/Client.ts index 85b6608..f3a5aa1 100644 --- a/src/services/Client.ts +++ b/src/services/Client.ts @@ -13,9 +13,11 @@ const __dirname = dirname(__filename); export const srcDir = path.join(__dirname, '..'); export default class BotClient extends Client { + private static instance: BotClient | null = null; public commands = new Collection(); // eslint-disable-next-line @typescript-eslint/no-explicit-any public t = new Collection(); + constructor() { super({ intents: [GatewayIntentBits.MessageContent], @@ -28,6 +30,11 @@ export default class BotClient extends Client { ], }, }); + BotClient.instance = this; + } + + public static getInstance(): BotClient | null { + return BotClient.instance; } public async init() { @@ -76,6 +83,11 @@ export default class BotClient extends Client { } public async getLocaleText(key: string, locale: string, replaces = {}): Promise { const fallbackLocale = 'en-US'; + + if (!locale) { + locale = fallbackLocale; + } + let langMap = this.t.get(locale); if (!langMap) { const langOnly = locale.split('-')[0]; diff --git a/src/types/express.d.ts b/src/types/express.d.ts new file mode 100644 index 0000000..b580dce --- /dev/null +++ b/src/types/express.d.ts @@ -0,0 +1,14 @@ +/// + +declare namespace Express { + interface Request { + user?: { + userId: string; + username: string; + discriminator: string; + avatar?: string; + iat?: number; + exp?: number; + }; + } +} diff --git a/src/utils/commandLogger.ts b/src/utils/commandLogger.ts index d50d1d6..75140c9 100644 --- a/src/utils/commandLogger.ts +++ b/src/utils/commandLogger.ts @@ -11,7 +11,7 @@ export interface CommandLogOptions { } export function logUserAction(options: CommandLogOptions): void { - const { commandName, userId, username, guildId, channelId } = options; + const { commandName, userId, username, guildId, channelId, additionalInfo } = options; let logMessage = `User ${username} (${userId}) used ${commandName} command`; @@ -23,6 +23,10 @@ export function logUserAction(options: CommandLogOptions): void { logMessage += ` in channel ${channelId}`; } + if (additionalInfo) { + logMessage += ` with ${additionalInfo}`; + } + logger.info(logMessage); } @@ -37,6 +41,7 @@ export function logUserActionFromInteraction( username: interaction.user.tag, guildId: interaction.guildId || undefined, channelId: interaction.channelId, + additionalInfo, }); } @@ -46,7 +51,7 @@ export function createCommandLogger(commandName: string) { logUserAction({ ...options, commandName }); }, logFromInteraction: (interaction: CommandInteraction, additionalInfo?: string) => { - logUserActionFromInteraction(interaction, commandName); + logUserActionFromInteraction(interaction, commandName, additionalInfo); }, }; } diff --git a/src/utils/encryption.ts b/src/utils/encryption.ts new file mode 100644 index 0000000..afcf868 --- /dev/null +++ b/src/utils/encryption.ts @@ -0,0 +1,82 @@ +import crypto from 'crypto'; +import { API_KEY_ENCRYPTION_SECRET } from '../config'; + +const ENCRYPTION_KEY = API_KEY_ENCRYPTION_SECRET; +const ALGORITHM = 'aes-256-gcm'; + +const getEncryptionKey = (): Buffer => { + if (ENCRYPTION_KEY.length !== 32) { + throw new Error('ENCRYPTION_KEY must be exactly 32 characters long'); + } + return Buffer.from(ENCRYPTION_KEY, 'utf8'); +}; + +/** + * Encrypts a string using AES-256-GCM + * @param text The text to encrypt + * @returns Base64 encoded encrypted data with IV and auth tag + */ +export const encryptApiKey = (text: string): string => { + try { + const key = getEncryptionKey(); + const iv = crypto.randomBytes(16); + + const cipher = crypto.createCipheriv(ALGORITHM, key, iv); + cipher.setAAD(Buffer.from('aethel-api-key', 'utf8')); + + let encrypted = cipher.update(text, 'utf8', 'hex'); + encrypted += cipher.final('hex'); + + const authTag = cipher.getAuthTag(); + + const combined = Buffer.concat([iv, authTag, Buffer.from(encrypted, 'hex')]); + + return combined.toString('base64'); + } catch { + throw new Error('Failed to encrypt API key'); + } +}; + +/** + * Decrypts a string that was encrypted with encryptApiKey + * @param encryptedData Base64 encoded encrypted data + * @returns The decrypted text + */ +export const decryptApiKey = (encryptedData: string): string => { + try { + const key = getEncryptionKey(); + const combined = Buffer.from(encryptedData, 'base64'); + + const extractedIv = combined.subarray(0, 16); + const authTag = combined.subarray(16, 32); + const encrypted = combined.subarray(32); + + const decipher = crypto.createDecipheriv(ALGORITHM, key, extractedIv); + decipher.setAAD(Buffer.from('aethel-api-key', 'utf8')); + decipher.setAuthTag(authTag); + + let decrypted = decipher.update(encrypted, undefined, 'utf8'); + decrypted += decipher.final('utf8'); + + return decrypted; + } catch { + throw new Error('Failed to decrypt API key'); + } +}; + +/** + * Generates a secure random encryption key + * @returns A 32-character random string suitable for use as ENCRYPTION_KEY + */ +export const generateEncryptionKey = (): string => { + return crypto.randomBytes(32).toString('base64').substring(0, 32); +}; + +/** + * Validates that an encryption key is properly formatted + * @param key The key to validate + * @returns True if the key is valid + */ +export const validateEncryptionKey = (key: string): boolean => { + return typeof key === 'string' && key.length === 32; +}; diff --git a/src/utils/reminderDb.ts b/src/utils/reminderDb.ts index 90a55cc..72c0b5d 100644 --- a/src/utils/reminderDb.ts +++ b/src/utils/reminderDb.ts @@ -197,6 +197,22 @@ async function cleanupReminders(days = 30) { } } +async function clearCompletedReminders(userId: string) { + const query = ` + DELETE FROM reminders + WHERE user_id = $1 + AND is_completed = TRUE + RETURNING * + `; + + try { + const result = await pool.query(query, [userId]); + return result.rowCount; + } catch (error) { + throw createDatabaseError(error, 'clearing completed reminders'); + } +} + export { saveReminder, completeReminder, @@ -204,6 +220,7 @@ export { getReminder, getUserReminders, cleanupReminders, + clearCompletedReminders, ensureUserRegistered, DatabaseError, }; diff --git a/tsconfig.json b/tsconfig.json index 99817b5..baf06f4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -105,7 +105,7 @@ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */ }, - "include": ["src/**/*"], + "include": ["src/**/*", "environment.d.ts"], "exclude": ["express-server/*", "node_modules", "dist"], "ts-node": { "pretty": true, diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..e1aae3f --- /dev/null +++ b/web/index.html @@ -0,0 +1,16 @@ + + + + + + + Aethel Dashboard + + + + + +
+ + + \ No newline at end of file diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..ffaa471 --- /dev/null +++ b/web/package.json @@ -0,0 +1,36 @@ +{ + "name": "aethel-dashboard", + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0" + }, + "dependencies": { + "@tanstack/react-query": "^5.83.0", + "axios": "^1.10.0", + "lucide-react": "^0.294.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.30.1", + "sonner": "^1.7.4", + "zustand": "^4.5.7" + }, + "devDependencies": { + "@types/react": "^18.3.23", + "@types/react-dom": "^18.3.7", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "@typescript-eslint/parser": "^6.21.0", + "@vitejs/plugin-react": "^4.7.0", + "autoprefixer": "^10.4.21", + "eslint": "^8.57.1", + "eslint-plugin-react-hooks": "^4.6.2", + "eslint-plugin-react-refresh": "^0.4.20", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.17", + "typescript": "^5.8.3", + "vite": "^4.5.14" + } +} diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml new file mode 100644 index 0000000..1b795a7 --- /dev/null +++ b/web/pnpm-lock.yaml @@ -0,0 +1,2724 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@tanstack/react-query': + specifier: ^5.83.0 + version: 5.83.0(react@18.3.1) + axios: + specifier: ^1.10.0 + version: 1.10.0 + lucide-react: + specifier: ^0.294.0 + version: 0.294.0(react@18.3.1) + react: + specifier: ^18.3.1 + version: 18.3.1 + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + react-router-dom: + specifier: ^6.30.1 + version: 6.30.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + sonner: + specifier: ^1.7.4 + version: 1.7.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + zustand: + specifier: ^4.5.7 + version: 4.5.7(@types/react@18.3.23)(react@18.3.1) + devDependencies: + '@types/react': + specifier: ^18.3.23 + version: 18.3.23 + '@types/react-dom': + specifier: ^18.3.7 + version: 18.3.7(@types/react@18.3.23) + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.8.3) + '@vitejs/plugin-react': + specifier: ^4.7.0 + version: 4.7.0(vite@4.5.14) + autoprefixer: + specifier: ^10.4.21 + version: 10.4.21(postcss@8.5.6) + eslint: + specifier: ^8.57.1 + version: 8.57.1 + eslint-plugin-react-hooks: + specifier: ^4.6.2 + version: 4.6.2(eslint@8.57.1) + eslint-plugin-react-refresh: + specifier: ^0.4.20 + version: 0.4.20(eslint@8.57.1) + postcss: + specifier: ^8.5.6 + version: 8.5.6 + tailwindcss: + specifier: ^3.4.17 + version: 3.4.17 + typescript: + specifier: ^5.8.3 + version: 5.8.3 + vite: + specifier: ^4.5.14 + version: 4.5.14 + +packages: + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.28.0': + resolution: {integrity: sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.28.0': + resolution: {integrity: sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.28.0': + resolution: {integrity: sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.27.2': + resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.27.1': + resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.27.3': + resolution: {integrity: sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.27.1': + resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.27.6': + resolution: {integrity: sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.0': + resolution: {integrity: sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.27.2': + resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.28.0': + resolution: {integrity: sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.28.1': + resolution: {integrity: sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==} + engines: {node: '>=6.9.0'} + + '@esbuild/android-arm64@0.18.20': + resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.18.20': + resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.18.20': + resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.18.20': + resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.18.20': + resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.18.20': + resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.18.20': + resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.18.20': + resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.18.20': + resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.18.20': + resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.18.20': + resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.18.20': + resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.18.20': + resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.18.20': + resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.18.20': + resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.18.20': + resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.18.20': + resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.18.20': + resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.18.20': + resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.18.20': + resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.18.20': + resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.18.20': + resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.7.0': + resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/eslintrc@2.1.4': + resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@eslint/js@8.57.1': + resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@humanwhocodes/config-array@0.13.0': + resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} + engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/object-schema@2.0.3': + resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + deprecated: Use @eslint/object-schema instead + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@jridgewell/gen-mapping@0.3.12': + resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.4': + resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==} + + '@jridgewell/trace-mapping@0.3.29': + resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==} + + '@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'} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@remix-run/router@1.23.0': + resolution: {integrity: sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==} + engines: {node: '>=14.0.0'} + + '@rolldown/pluginutils@1.0.0-beta.27': + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + + '@tanstack/query-core@5.83.0': + resolution: {integrity: sha512-0M8dA+amXUkyz5cVUm/B+zSk3xkQAcuXuz5/Q/LveT4ots2rBpPTZOzd7yJa2Utsf8D2Upl5KyjhHRY+9lB/XA==} + + '@tanstack/react-query@5.83.0': + resolution: {integrity: sha512-/XGYhZ3foc5H0VM2jLSD/NyBRIOK4q9kfeml4+0x2DlL6xVuAcVEW+hTlTapAmejObg0i3eNqhkr2dT+eciwoQ==} + peerDependencies: + react: ^18 || ^19 + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.20.7': + resolution: {integrity: sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + + '@types/react-dom@18.3.7': + resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} + peerDependencies: + '@types/react': ^18.0.0 + + '@types/react@18.3.23': + resolution: {integrity: sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==} + + '@types/semver@7.7.0': + resolution: {integrity: sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==} + + '@typescript-eslint/eslint-plugin@6.21.0': + resolution: {integrity: sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/parser@6.21.0': + resolution: {integrity: sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/scope-manager@6.21.0': + resolution: {integrity: sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==} + engines: {node: ^16.0.0 || >=18.0.0} + + '@typescript-eslint/type-utils@6.21.0': + resolution: {integrity: sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/types@6.21.0': + resolution: {integrity: sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==} + engines: {node: ^16.0.0 || >=18.0.0} + + '@typescript-eslint/typescript-estree@6.21.0': + resolution: {integrity: sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/utils@6.21.0': + resolution: {integrity: sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + + '@typescript-eslint/visitor-keys@6.21.0': + resolution: {integrity: sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==} + engines: {node: ^16.0.0 || >=18.0.0} + + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + + '@vitejs/plugin-react@4.7.0': + resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.1.0: + resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + autoprefixer@10.4.21: + resolution: {integrity: sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + + axios@1.10.0: + resolution: {integrity: sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.25.1: + resolution: {integrity: sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camelcase-css@2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + + caniuse-lite@1.0.30001727: + resolution: {integrity: sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + debug@4.4.1: + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + + doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + electron-to-chromium@1.5.189: + resolution: {integrity: sha512-y9D1ntS1ruO/pZ/V2FtLE+JXLQe28XoRpZ7QCCo0T8LdQladzdcOVQZH/IWLVJvCw12OGMb6hYOeOAjntCmJRQ==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + esbuild@0.18.20: + resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} + engines: {node: '>=12'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-plugin-react-hooks@4.6.2: + resolution: {integrity: sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==} + engines: {node: '>=10'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 + + eslint-plugin-react-refresh@0.4.20: + resolution: {integrity: sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==} + peerDependencies: + eslint: '>=8.40' + + eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint@8.57.1: + resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. + hasBin: true + + espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + + file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + follow-redirects@1.15.9: + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + form-data@4.0.4: + resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} + engines: {node: '>= 6'} + + fraction.js@4.3.7: + resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + hasBin: true + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + + globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + jiti@1.21.7: + resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lucide-react@0.294.0: + resolution: {integrity: sha512-V7o0/VECSGbLHn3/1O67FUgBwWB+hmzshrgDVRJQhMh8uj5D3HBuIvhuAmQTtlupILSplwIZg5FTc4tTKMA2SA==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@9.0.3: + resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} + engines: {node: '>=16 || 14 >=14.17'} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + node-releases@2.0.19: + resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + normalize-range@0.1.2: + resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} + engines: {node: '>=0.10.0'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + postcss-import@15.1.0: + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: ^8.0.0 + + postcss-js@4.0.1: + resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + + postcss-load-config@4.0.2: + resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} + engines: {node: '>= 14'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + + postcss-nested@6.2.0: + resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + + react-router-dom@6.30.1: + resolution: {integrity: sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + + react-router@6.30.1: + resolution: {integrity: sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: '>=16.8' + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + + read-cache@1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve@1.22.10: + resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} + engines: {node: '>= 0.4'} + hasBin: true + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rollup@3.29.5: + resolution: {integrity: sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==} + engines: {node: '>=14.18.0', npm: '>=8.0.0'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + sonner@1.7.4: + resolution: {integrity: sha512-DIS8z4PfJRbIyfVFDVnK9rO3eYDtse4Omcm6bt0oEr5/jtLgysmjuBl1frJ9E/EQZrFmKx2A8m/s5s9CRXIzhw==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + 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'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + sucrase@3.35.0: + resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tailwindcss@3.4.17: + resolution: {integrity: sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==} + engines: {node: '>=14.0.0'} + hasBin: true + + text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + ts-api-utils@1.4.3: + resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} + engines: {node: '>=16'} + peerDependencies: + typescript: '>=4.2.0' + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + typescript@5.8.3: + resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + engines: {node: '>=14.17'} + hasBin: true + + update-browserslist-db@1.1.3: + resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + use-sync-external-store@1.5.0: + resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vite@4.5.14: + resolution: {integrity: sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==} + engines: {node: ^14.18.0 || >=16.0.0} + hasBin: true + peerDependencies: + '@types/node': '>= 14' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yaml@2.8.0: + resolution: {integrity: sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==} + engines: {node: '>= 14.6'} + hasBin: true + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zustand@4.5.7: + resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} + engines: {node: '>=12.7.0'} + peerDependencies: + '@types/react': '>=16.8' + immer: '>=9.0.6' + react: '>=16.8' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + +snapshots: + + '@alloc/quick-lru@5.2.0': {} + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.12 + '@jridgewell/trace-mapping': 0.3.29 + + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.27.1 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.28.0': {} + + '@babel/core@7.28.0': + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.0 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0) + '@babel/helpers': 7.27.6 + '@babel/parser': 7.28.0 + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.0 + '@babel/types': 7.28.1 + convert-source-map: 2.0.0 + debug: 4.4.1 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.28.0': + dependencies: + '@babel/parser': 7.28.0 + '@babel/types': 7.28.1 + '@jridgewell/gen-mapping': 0.3.12 + '@jridgewell/trace-mapping': 0.3.29 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.27.2': + dependencies: + '@babel/compat-data': 7.28.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.25.1 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.27.1': + dependencies: + '@babel/traverse': 7.28.0 + '@babel/types': 7.28.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.27.3(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.28.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.27.1': {} + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.27.1': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.27.6': + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.28.1 + + '@babel/parser@7.28.0': + dependencies: + '@babel/types': 7.28.1 + + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/template@7.27.2': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.28.0 + '@babel/types': 7.28.1 + + '@babel/traverse@7.28.0': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.0 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.0 + '@babel/template': 7.27.2 + '@babel/types': 7.28.1 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.28.1': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + + '@esbuild/android-arm64@0.18.20': + optional: true + + '@esbuild/android-arm@0.18.20': + optional: true + + '@esbuild/android-x64@0.18.20': + optional: true + + '@esbuild/darwin-arm64@0.18.20': + optional: true + + '@esbuild/darwin-x64@0.18.20': + optional: true + + '@esbuild/freebsd-arm64@0.18.20': + optional: true + + '@esbuild/freebsd-x64@0.18.20': + optional: true + + '@esbuild/linux-arm64@0.18.20': + optional: true + + '@esbuild/linux-arm@0.18.20': + optional: true + + '@esbuild/linux-ia32@0.18.20': + optional: true + + '@esbuild/linux-loong64@0.18.20': + optional: true + + '@esbuild/linux-mips64el@0.18.20': + optional: true + + '@esbuild/linux-ppc64@0.18.20': + optional: true + + '@esbuild/linux-riscv64@0.18.20': + optional: true + + '@esbuild/linux-s390x@0.18.20': + optional: true + + '@esbuild/linux-x64@0.18.20': + optional: true + + '@esbuild/netbsd-x64@0.18.20': + optional: true + + '@esbuild/openbsd-x64@0.18.20': + optional: true + + '@esbuild/sunos-x64@0.18.20': + optional: true + + '@esbuild/win32-arm64@0.18.20': + optional: true + + '@esbuild/win32-ia32@0.18.20': + optional: true + + '@esbuild/win32-x64@0.18.20': + optional: true + + '@eslint-community/eslint-utils@4.7.0(eslint@8.57.1)': + dependencies: + eslint: 8.57.1 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.1': {} + + '@eslint/eslintrc@2.1.4': + dependencies: + ajv: 6.12.6 + debug: 4.4.1 + espree: 9.6.1 + globals: 13.24.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@8.57.1': {} + + '@humanwhocodes/config-array@0.13.0': + dependencies: + '@humanwhocodes/object-schema': 2.0.3 + debug: 4.4.1 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/object-schema@2.0.3': {} + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@jridgewell/gen-mapping@0.3.12': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.4 + '@jridgewell/trace-mapping': 0.3.29 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.4': {} + + '@jridgewell/trace-mapping@0.3.29': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.4 + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.19.1 + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@remix-run/router@1.23.0': {} + + '@rolldown/pluginutils@1.0.0-beta.27': {} + + '@tanstack/query-core@5.83.0': {} + + '@tanstack/react-query@5.83.0(react@18.3.1)': + dependencies: + '@tanstack/query-core': 5.83.0 + react: 18.3.1 + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.28.0 + '@babel/types': 7.28.1 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.20.7 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.28.1 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.28.0 + '@babel/types': 7.28.1 + + '@types/babel__traverse@7.20.7': + dependencies: + '@babel/types': 7.28.1 + + '@types/json-schema@7.0.15': {} + + '@types/prop-types@15.7.15': {} + + '@types/react-dom@18.3.7(@types/react@18.3.23)': + dependencies: + '@types/react': 18.3.23 + + '@types/react@18.3.23': + dependencies: + '@types/prop-types': 15.7.15 + csstype: 3.1.3 + + '@types/semver@7.7.0': {} + + '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3)': + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.8.3) + '@typescript-eslint/scope-manager': 6.21.0 + '@typescript-eslint/type-utils': 6.21.0(eslint@8.57.1)(typescript@5.8.3) + '@typescript-eslint/utils': 6.21.0(eslint@8.57.1)(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 6.21.0 + debug: 4.4.1 + eslint: 8.57.1 + graphemer: 1.4.0 + ignore: 5.3.2 + natural-compare: 1.4.0 + semver: 7.7.2 + ts-api-utils: 1.4.3(typescript@5.8.3) + optionalDependencies: + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3)': + dependencies: + '@typescript-eslint/scope-manager': 6.21.0 + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 6.21.0 + debug: 4.4.1 + eslint: 8.57.1 + optionalDependencies: + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@6.21.0': + dependencies: + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/visitor-keys': 6.21.0 + + '@typescript-eslint/type-utils@6.21.0(eslint@8.57.1)(typescript@5.8.3)': + dependencies: + '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.8.3) + '@typescript-eslint/utils': 6.21.0(eslint@8.57.1)(typescript@5.8.3) + debug: 4.4.1 + eslint: 8.57.1 + ts-api-utils: 1.4.3(typescript@5.8.3) + optionalDependencies: + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@6.21.0': {} + + '@typescript-eslint/typescript-estree@6.21.0(typescript@5.8.3)': + dependencies: + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/visitor-keys': 6.21.0 + debug: 4.4.1 + globby: 11.1.0 + is-glob: 4.0.3 + minimatch: 9.0.3 + semver: 7.7.2 + ts-api-utils: 1.4.3(typescript@5.8.3) + optionalDependencies: + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@6.21.0(eslint@8.57.1)(typescript@5.8.3)': + dependencies: + '@eslint-community/eslint-utils': 4.7.0(eslint@8.57.1) + '@types/json-schema': 7.0.15 + '@types/semver': 7.7.0 + '@typescript-eslint/scope-manager': 6.21.0 + '@typescript-eslint/types': 6.21.0 + '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.8.3) + eslint: 8.57.1 + semver: 7.7.2 + transitivePeerDependencies: + - supports-color + - typescript + + '@typescript-eslint/visitor-keys@6.21.0': + dependencies: + '@typescript-eslint/types': 6.21.0 + eslint-visitor-keys: 3.4.3 + + '@ungap/structured-clone@1.3.0': {} + + '@vitejs/plugin-react@4.7.0(vite@4.5.14)': + dependencies: + '@babel/core': 7.28.0 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.0) + '@rolldown/pluginutils': 1.0.0-beta.27 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 4.5.14 + transitivePeerDependencies: + - supports-color + + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-regex@5.0.1: {} + + ansi-regex@6.1.0: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.1: {} + + any-promise@1.3.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + arg@5.0.2: {} + + argparse@2.0.1: {} + + array-union@2.1.0: {} + + asynckit@0.4.0: {} + + autoprefixer@10.4.21(postcss@8.5.6): + dependencies: + browserslist: 4.25.1 + caniuse-lite: 1.0.30001727 + fraction.js: 4.3.7 + normalize-range: 0.1.2 + picocolors: 1.1.1 + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + axios@1.10.0: + dependencies: + follow-redirects: 1.15.9 + form-data: 4.0.4 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + balanced-match@1.0.2: {} + + binary-extensions@2.3.0: {} + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.25.1: + dependencies: + caniuse-lite: 1.0.30001727 + electron-to-chromium: 1.5.189 + node-releases: 2.0.19 + update-browserslist-db: 1.1.3(browserslist@4.25.1) + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + callsites@3.1.0: {} + + camelcase-css@2.0.1: {} + + caniuse-lite@1.0.30001727: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commander@4.1.1: {} + + concat-map@0.0.1: {} + + convert-source-map@2.0.0: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + cssesc@3.0.0: {} + + csstype@3.1.3: {} + + debug@4.4.1: + dependencies: + ms: 2.1.3 + + deep-is@0.1.4: {} + + delayed-stream@1.0.0: {} + + didyoumean@1.2.2: {} + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + dlv@1.1.3: {} + + doctrine@3.0.0: + dependencies: + esutils: 2.0.3 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + eastasianwidth@0.2.0: {} + + electron-to-chromium@1.5.189: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + esbuild@0.18.20: + optionalDependencies: + '@esbuild/android-arm': 0.18.20 + '@esbuild/android-arm64': 0.18.20 + '@esbuild/android-x64': 0.18.20 + '@esbuild/darwin-arm64': 0.18.20 + '@esbuild/darwin-x64': 0.18.20 + '@esbuild/freebsd-arm64': 0.18.20 + '@esbuild/freebsd-x64': 0.18.20 + '@esbuild/linux-arm': 0.18.20 + '@esbuild/linux-arm64': 0.18.20 + '@esbuild/linux-ia32': 0.18.20 + '@esbuild/linux-loong64': 0.18.20 + '@esbuild/linux-mips64el': 0.18.20 + '@esbuild/linux-ppc64': 0.18.20 + '@esbuild/linux-riscv64': 0.18.20 + '@esbuild/linux-s390x': 0.18.20 + '@esbuild/linux-x64': 0.18.20 + '@esbuild/netbsd-x64': 0.18.20 + '@esbuild/openbsd-x64': 0.18.20 + '@esbuild/sunos-x64': 0.18.20 + '@esbuild/win32-arm64': 0.18.20 + '@esbuild/win32-ia32': 0.18.20 + '@esbuild/win32-x64': 0.18.20 + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-plugin-react-hooks@4.6.2(eslint@8.57.1): + dependencies: + eslint: 8.57.1 + + eslint-plugin-react-refresh@0.4.20(eslint@8.57.1): + dependencies: + eslint: 8.57.1 + + eslint-scope@7.2.2: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint@8.57.1: + dependencies: + '@eslint-community/eslint-utils': 4.7.0(eslint@8.57.1) + '@eslint-community/regexpp': 4.12.1 + '@eslint/eslintrc': 2.1.4 + '@eslint/js': 8.57.1 + '@humanwhocodes/config-array': 0.13.0 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + '@ungap/structured-clone': 1.3.0 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.1 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.24.0 + graphemer: 1.4.0 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + + espree@9.6.1: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 3.4.3 + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.3: + dependencies: + '@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 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fastq@1.19.1: + dependencies: + reusify: 1.1.0 + + file-entry-cache@6.0.1: + dependencies: + flat-cache: 3.2.0 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@3.2.0: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + rimraf: 3.0.2 + + flatted@3.3.3: {} + + follow-redirects@1.15.9: {} + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + form-data@4.0.4: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + fraction.js@4.3.7: {} + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + gensync@1.0.0-beta.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@10.4.5: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + globals@13.24.0: + dependencies: + type-fest: 0.20.2 + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + gopd@1.2.0: {} + + graphemer@1.4.0: {} + + has-flag@4.0.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + ignore@5.3.2: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-path-inside@3.0.3: {} + + isexe@2.0.0: {} + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + jiti@1.21.7: {} + + js-tokens@4.0.0: {} + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lilconfig@3.1.3: {} + + lines-and-columns@1.2.4: {} + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lru-cache@10.4.3: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lucide-react@0.294.0(react@18.3.1): + dependencies: + react: 18.3.1 + + math-intrinsics@1.1.0: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minimatch@9.0.3: + dependencies: + brace-expansion: 2.0.2 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + + minipass@7.1.2: {} + + ms@2.1.3: {} + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + nanoid@3.3.11: {} + + natural-compare@1.4.0: {} + + node-releases@2.0.19: {} + + normalize-path@3.0.0: {} + + normalize-range@0.1.2: {} + + object-assign@4.1.1: {} + + object-hash@3.0.0: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + package-json-from-dist@1.0.1: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + path-exists@4.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + + path-type@4.0.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + pify@2.3.0: {} + + pirates@4.0.7: {} + + postcss-import@15.1.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.10 + + postcss-js@4.0.1(postcss@8.5.6): + dependencies: + camelcase-css: 2.0.1 + postcss: 8.5.6 + + postcss-load-config@4.0.2(postcss@8.5.6): + dependencies: + lilconfig: 3.1.3 + yaml: 2.8.0 + optionalDependencies: + postcss: 8.5.6 + + postcss-nested@6.2.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 6.1.2 + + postcss-selector-parser@6.1.2: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-value-parser@4.2.0: {} + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + proxy-from-env@1.1.0: {} + + punycode@2.3.1: {} + + queue-microtask@1.2.3: {} + + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + + react-refresh@0.17.0: {} + + react-router-dom@6.30.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@remix-run/router': 1.23.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-router: 6.30.1(react@18.3.1) + + react-router@6.30.1(react@18.3.1): + dependencies: + '@remix-run/router': 1.23.0 + react: 18.3.1 + + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + + read-cache@1.0.0: + dependencies: + pify: 2.3.0 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + resolve-from@4.0.0: {} + + resolve@1.22.10: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + reusify@1.1.0: {} + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + + rollup@3.29.5: + optionalDependencies: + fsevents: 2.3.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + + semver@6.3.1: {} + + semver@7.7.2: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + signal-exit@4.1.0: {} + + slash@3.0.0: {} + + sonner@1.7.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + source-map-js@1.2.1: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.0: + dependencies: + ansi-regex: 6.1.0 + + strip-json-comments@3.1.1: {} + + sucrase@3.35.0: + dependencies: + '@jridgewell/gen-mapping': 0.3.12 + commander: 4.1.1 + glob: 10.4.5 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + ts-interface-checker: 0.1.13 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + tailwindcss@3.4.17: + dependencies: + '@alloc/quick-lru': 5.2.0 + arg: 5.0.2 + chokidar: 3.6.0 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.3.3 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.21.7 + lilconfig: 3.1.3 + micromatch: 4.0.8 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.1.1 + postcss: 8.5.6 + postcss-import: 15.1.0(postcss@8.5.6) + postcss-js: 4.0.1(postcss@8.5.6) + postcss-load-config: 4.0.2(postcss@8.5.6) + postcss-nested: 6.2.0(postcss@8.5.6) + postcss-selector-parser: 6.1.2 + resolve: 1.22.10 + sucrase: 3.35.0 + transitivePeerDependencies: + - ts-node + + text-table@0.2.0: {} + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + ts-api-utils@1.4.3(typescript@5.8.3): + dependencies: + typescript: 5.8.3 + + ts-interface-checker@0.1.13: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@0.20.2: {} + + typescript@5.8.3: {} + + update-browserslist-db@1.1.3(browserslist@4.25.1): + dependencies: + browserslist: 4.25.1 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + use-sync-external-store@1.5.0(react@18.3.1): + dependencies: + react: 18.3.1 + + util-deprecate@1.0.2: {} + + vite@4.5.14: + dependencies: + esbuild: 0.18.20 + postcss: 8.5.6 + rollup: 3.29.5 + optionalDependencies: + fsevents: 2.3.3 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + + wrappy@1.0.2: {} + + yallist@3.1.1: {} + + yaml@2.8.0: {} + + yocto-queue@0.1.0: {} + + zustand@4.5.7(@types/react@18.3.23)(react@18.3.1): + dependencies: + use-sync-external-store: 1.5.0(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + react: 18.3.1 diff --git a/web/postcss.config.js b/web/postcss.config.js new file mode 100644 index 0000000..2aa7205 --- /dev/null +++ b/web/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/web/src/App.tsx b/web/src/App.tsx new file mode 100644 index 0000000..8f47fc1 --- /dev/null +++ b/web/src/App.tsx @@ -0,0 +1,51 @@ +import { Routes, Route, Navigate } from 'react-router-dom' +import { useAuthStore } from './stores/authStore' +import LandingPage from './pages/LandingPage' +import LoginPage from './pages/LoginPage' +import StatusPage from './pages/StatusPage' +import PrivacyPage from './pages/PrivacyPage' +import TermsPage from './pages/TermsPage' +import DashboardPage from './pages/DashboardPage' +import TodosPage from './pages/TodosPage' +import ApiKeysPage from './pages/ApiKeysPage' +import RemindersPage from './pages/RemindersPage' +import Layout from './components/Layout' +import { useEffect } from 'react' + +function App() { + const { isAuthenticated, checkAuth } = useAuthStore() + + useEffect(() => { + checkAuth() + }, []) + + return ( + + {/* Public routes */} + } /> + : } /> + } /> + } /> + } /> + + {/* Protected routes */} + {isAuthenticated ? ( + + + } /> + } /> + } /> + } /> + } /> + + + } /> + ) : ( + } /> + )} + + ) +} + +export default App \ No newline at end of file diff --git a/web/src/components/Layout.tsx b/web/src/components/Layout.tsx new file mode 100644 index 0000000..f89b883 --- /dev/null +++ b/web/src/components/Layout.tsx @@ -0,0 +1,167 @@ +import { ReactNode, useState } from 'react' +import { Link, useLocation } from 'react-router-dom' +import { + LayoutDashboard, + CheckSquare, + Key, + Bell, + Menu, + X, + LogOut, + User +} from 'lucide-react' +import { useAuthStore } from '../stores/authStore' +import { toast } from 'sonner' + +interface LayoutProps { + children: ReactNode +} + +const Layout = ({ children }: LayoutProps) => { + const [sidebarOpen, setSidebarOpen] = useState(false) + const location = useLocation() + const { user, logout } = useAuthStore() + + const navigation = [ + { name: 'Dashboard', href: '/dashboard', icon: LayoutDashboard }, + { name: 'Todos', href: '/todos', icon: CheckSquare }, + { name: 'Reminders', href: '/reminders', icon: Bell }, + { name: 'API Keys', href: '/api-keys', icon: Key }, + ] + + const handleLogout = () => { + logout() + toast.success('Logged out successfully') + } + + return ( +
+ {/* Mobile sidebar */} +
+
setSidebarOpen(false)} /> +
+
+

Aethel

+ +
+ +
+
+ + {/* Desktop sidebar */} +
+
+
+

Aethel Dashboard

+
+ + + {/* User info and logout */} +
+
+
+ {user?.avatar ? ( + {user.username} + ) : ( +
+ +
+ )} +
+
+

+ {user?.discriminator && user.discriminator !== '0' + ? `${user.username}#${user.discriminator}` + : user?.username} +

+
+
+ +
+
+
+ + {/* Main content */} +
+ {/* Mobile header */} +
+ +
+ Aethel Dashboard +
+
+ + {/* Page content */} +
+
+ {children} +
+
+
+
+ ) +} + +export default Layout \ No newline at end of file diff --git a/web/src/index.css b/web/src/index.css new file mode 100644 index 0000000..99669e3 --- /dev/null +++ b/web/src/index.css @@ -0,0 +1,35 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + font-family: Inter, system-ui, sans-serif; +} + +.btn { + @apply inline-flex items-center justify-center rounded-lg px-4 py-2 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50; +} + +.btn-primary { + @apply bg-blue-600 text-white hover:bg-blue-700; +} + +.btn-secondary { + @apply bg-gray-700 text-white hover:bg-gray-600 border border-gray-600; +} + +.btn-danger { + @apply bg-red-600 text-white hover:bg-red-700; +} + +.btn-success { + @apply bg-green-600 text-white hover:bg-green-700; +} + +.card { + @apply bg-gray-900/50 rounded-lg border border-gray-700 shadow-sm; +} + +.input { + @apply flex h-10 w-full rounded-lg border border-gray-600 bg-gray-800 text-white px-3 py-2 text-sm placeholder:text-gray-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-500 focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50; +} \ No newline at end of file diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts new file mode 100644 index 0000000..42fa873 --- /dev/null +++ b/web/src/lib/api.ts @@ -0,0 +1,73 @@ +import axios from 'axios'; +import { toast } from 'sonner'; + +const api = axios.create({ + baseURL: '/api', + timeout: 10000, +}); + +api.interceptors.request.use( + (config) => { + const token = localStorage.getItem('token'); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; + }, + (error) => { + return Promise.reject(error); + } +); + +api.interceptors.response.use( + (response) => response, + (error) => { + if (error.response?.status === 401) { + localStorage.removeItem('token'); + window.location.href = '/'; + toast.error('Session expired. Please login again.'); + } else if (error.response?.status >= 500) { + toast.error('Server error. Please try again later.'); + } else if (error.response?.data?.message) { + toast.error(error.response.data.message); + } else { + toast.error('An unexpected error occurred.'); + } + return Promise.reject(error); + } +); + +export default api; + +export const authAPI = { + getDiscordAuthUrl: () => api.get('/auth/discord'), + getMe: () => api.get('/auth/me'), + logout: () => api.post('/auth/logout'), +}; + +export const todosAPI = { + getTodos: () => api.get('/todos'), + createTodo: (data: { item: string }) => api.post('/todos', data), + updateTodo: (id: number, data: { item?: string; done?: boolean }) => + api.put(`/todos/${id}`, data), + deleteTodo: (id: number) => api.delete(`/todos/${id}`), + clearTodos: () => api.delete('/todos'), +}; + +export const apiKeysAPI = { + getApiKeys: () => api.get('/user/api-keys'), + updateApiKey: (data: { apiKey?: string; model?: string; apiUrl?: string }) => + api.post('/user/api-keys', data), + deleteApiKey: () => api.delete('/user/api-keys'), + testApiKey: (data: { apiKey: string; model?: string; apiUrl?: string }) => + api.post('/user/api-keys/test', data), +}; + +export const remindersAPI = { + getReminders: () => api.get('/reminders'), + createReminder: (data: { message: string; expires_at: string }) => api.post('/reminders', data), + getReminder: (id: string) => api.get(`/reminders/${id}`), + completeReminder: (id: string) => api.patch(`/reminders/${id}/complete`), + getActiveReminders: () => api.get('/reminders/active/all'), + clearCompletedReminders: () => api.delete('/reminders/completed'), +}; diff --git a/web/src/main.tsx b/web/src/main.tsx new file mode 100644 index 0000000..9f471e1 --- /dev/null +++ b/web/src/main.tsx @@ -0,0 +1,27 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { BrowserRouter } from 'react-router-dom' +import { Toaster } from 'sonner' +import App from './App.tsx' +import './index.css' + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: 1, + refetchOnWindowFocus: false, + }, + }, +}) + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + + + + + + +) \ No newline at end of file diff --git a/web/src/pages/ApiKeysPage.tsx b/web/src/pages/ApiKeysPage.tsx new file mode 100644 index 0000000..ff8a2ad --- /dev/null +++ b/web/src/pages/ApiKeysPage.tsx @@ -0,0 +1,340 @@ +import { useState } from 'react' +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' +import { Key, Eye, EyeOff, TestTube, Save, Trash2, AlertCircle, CheckCircle } from 'lucide-react' +import { toast } from 'sonner' +import { apiKeysAPI } from '../lib/api' + +const ApiKeysPage = () => { + const [showApiKey, setShowApiKey] = useState(false) + const [formData, setFormData] = useState({ + apiKey: '', + model: '', + apiUrl: '' + }) + const [isEditing, setIsEditing] = useState(false) + const [testResult, setTestResult] = useState<{ success: boolean; message: string } | null>(null) + const queryClient = useQueryClient() + + const { data: apiKeyInfo, isLoading } = useQuery({ + queryKey: ['api-keys'], + queryFn: () => apiKeysAPI.getApiKeys().then(res => res.data), + }) + + const updateApiKeyMutation = useMutation({ + mutationFn: (data: { apiKey: string; model?: string; apiUrl?: string }) => + apiKeysAPI.updateApiKey(data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['api-keys'] }) + setIsEditing(false) + setFormData({ apiKey: '', model: '', apiUrl: '' }) + toast.success('API key updated successfully!') + }, + onError: () => { + toast.error('Failed to update API key') + }, + }) + + const deleteApiKeyMutation = useMutation({ + mutationFn: () => apiKeysAPI.deleteApiKey(), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['api-keys'] }) + setFormData({ apiKey: '', model: '', apiUrl: '' }) + setIsEditing(false) + toast.success('API key deleted successfully!') + }, + onError: () => { + toast.error('Failed to delete API key') + }, + }) + + const testApiKeyMutation = useMutation({ + mutationFn: (data: { apiKey: string; model?: string; apiUrl?: string }) => + apiKeysAPI.testApiKey(data), + onSuccess: () => { + setTestResult({ success: true, message: 'API key is valid and working!' }) + toast.success('API key test successful!') + }, + onError: (error: any) => { + const message = error.response?.data?.error || 'API key test failed' + setTestResult({ success: false, message }) + toast.error(message) + }, + }) + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + if (!formData.apiKey.trim()) { + toast.error('API key is required') + return + } + updateApiKeyMutation.mutate({ + apiKey: formData.apiKey, + model: formData.model || undefined, + apiUrl: formData.apiUrl || undefined, + }) + } + + const handleTest = () => { + if (!formData.apiKey.trim()) { + toast.error('API key is required for testing') + return + } + setTestResult(null) + testApiKeyMutation.mutate({ + apiKey: formData.apiKey, + model: formData.model || undefined, + apiUrl: formData.apiUrl || undefined, + }) + } + + const handleEdit = () => { + setIsEditing(true) + setFormData({ + apiKey: '', + model: apiKeyInfo?.model || '', + apiUrl: apiKeyInfo?.apiUrl || '' + }) + setTestResult(null) + } + + const handleCancel = () => { + setIsEditing(false) + setFormData({ apiKey: '', model: '', apiUrl: '' }) + setTestResult(null) + } + + if (isLoading) { + return ( +
+
+
+ ) + } + + return ( +
+ {/* Header */} +
+

AI API Keys

+

+ Configure your custom AI API keys and endpoints for personalized AI interactions. +

+
+ + {/* Current Status */} +
+
+

Current Configuration

+ {apiKeyInfo?.hasApiKey && !isEditing && ( +
+ + +
+ )} +
+ + {apiKeyInfo?.hasApiKey && !isEditing ? ( +
+
+ Status + + + Configured + +
+ {apiKeyInfo.model && ( +
+ Model + {apiKeyInfo.model} +
+ )} + {apiKeyInfo.apiUrl && ( +
+ API Endpoint + + {apiKeyInfo.apiUrl} + +
+ )} +
+ ) : ( +
+ +

+ No API Key Configured +

+

+ Set up your custom AI API key to use personalized models and endpoints. +

+ +
+ )} +
+ + {/* Configuration Form */} + {isEditing && ( +
+

+ {apiKeyInfo?.hasApiKey ? 'Update' : 'Configure'} API Key +

+ +
+ {/* API Key */} +
+ +
+ setFormData({ ...formData, apiKey: e.target.value })} + placeholder="Enter your API key" + className="input pr-10" + required + /> + +
+
+ + {/* Model */} +
+ + setFormData({ ...formData, model: e.target.value })} + placeholder="e.g., gpt-4, claude-3-opus" + className="input" + /> +

+ Leave empty to use the default model +

+
+ + {/* API URL */} +
+ + setFormData({ ...formData, apiUrl: e.target.value })} + placeholder="https://api.openai.com/v1/chat/completions" + className="input" + /> +

+ Enter the full API endpoint URL including the path (e.g., /chat/completions) +

+
+ + {/* Test Result */} + {testResult && ( +
+ {testResult.success ? ( + + ) : ( + + )} + {testResult.message} +
+ )} + + {/* Actions */} +
+ + +
+ + +
+
+
+
+ )} + + {/* Information */} +
+

Information

+
+
+ +

+ Your API key is encrypted and stored securely. It will only be used for AI interactions within the Discord bot. +

+
+
+ +

+ Supported providers include OpenAI, Anthropic, and any OpenAI-compatible API endpoints. +

+
+
+ +

+ Use the test function to verify your API key works before saving. +

+
+
+
+
+ ) +} + +export default ApiKeysPage \ No newline at end of file diff --git a/web/src/pages/DashboardPage.tsx b/web/src/pages/DashboardPage.tsx new file mode 100644 index 0000000..595fb27 --- /dev/null +++ b/web/src/pages/DashboardPage.tsx @@ -0,0 +1,338 @@ +import { useQuery } from '@tanstack/react-query' +import { CheckSquare, Key, Clock, TrendingUp, Bell, AlertCircle } from 'lucide-react' +import { todosAPI, apiKeysAPI, remindersAPI } from '../lib/api' +import { useAuthStore } from '../stores/authStore' +import { useEffect, useState } from 'react' +import { toast } from 'sonner' + +const DashboardPage = () => { + const { user } = useAuthStore() + const [notifications, setNotifications] = useState([]) + + const { data: todos } = useQuery({ + queryKey: ['todos'], + queryFn: () => todosAPI.getTodos().then(res => res.data), + }) + + const { data: apiKeyInfo } = useQuery({ + queryKey: ['api-keys'], + queryFn: () => apiKeysAPI.getApiKeys().then(res => res.data), + }) + + const { data: reminders } = useQuery({ + queryKey: ['reminders'], + queryFn: () => remindersAPI.getReminders().then(res => res.data.reminders), + }) + + const { data: activeReminders } = useQuery({ + queryKey: ['active-reminders'], + queryFn: () => remindersAPI.getActiveReminders().then(res => res.data.reminders), + refetchInterval: 30000, // Check every 30 seconds + }) + + const completedTodos = todos?.filter((todo: any) => todo.done).length || 0 + const pendingTodos = todos?.filter((todo: any) => !todo.done).length || 0 + const totalTodos = todos?.length || 0 + const hasApiKey = !!apiKeyInfo?.hasApiKey + + const activeRemindersCount = activeReminders?.length || 0 + const overdueReminders = activeReminders?.filter((reminder: any) => + new Date(reminder.expires_at) < new Date() + ) || [] + + const stats = [ + { + name: 'Total Todos', + value: totalTodos, + icon: CheckSquare, + color: 'text-blue-600', + bgColor: 'bg-blue-100', + }, + { + name: 'Completed', + value: completedTodos, + icon: TrendingUp, + color: 'text-green-600', + bgColor: 'bg-green-100', + }, + { + name: 'Pending', + value: pendingTodos, + icon: Clock, + color: 'text-yellow-600', + bgColor: 'bg-yellow-100', + }, + { + name: 'Active Reminders', + value: activeRemindersCount, + icon: Bell, + color: overdueReminders.length > 0 ? 'text-red-600' : 'text-blue-600', + bgColor: overdueReminders.length > 0 ? 'bg-red-100' : 'bg-blue-100', + }, + { + name: 'API Key', + value: hasApiKey ? 'Configured' : 'Not Set', + icon: Key, + color: hasApiKey ? 'text-green-600' : 'text-red-600', + bgColor: hasApiKey ? 'bg-green-100' : 'bg-red-100', + }, + ] + + const recentTodos = todos?.slice(0, 5) || [] + const recentReminders = reminders?.slice(0, 5) || [] + + useEffect(() => { + if (overdueReminders.length > 0) { + overdueReminders.forEach((reminder: any) => { + const notificationId = `reminder-${reminder.reminder_id}` + if (!notifications.includes(notificationId)) { + toast.error(`Reminder: ${reminder.message}`, { + duration: 10000, + action: { + label: 'Mark Complete', + onClick: () => handleCompleteReminder(reminder.reminder_id) + } + }) + setNotifications(prev => [...prev, notificationId]) + } + }) + } + }, [overdueReminders]) + + const handleCompleteReminder = async (id: string) => { + try { + await remindersAPI.completeReminder(id) + toast.success('Reminder completed!') + setNotifications(prev => prev.filter(notif => notif !== `reminder-${id}`)) + } catch (error) { + toast.error('Failed to complete reminder') + } + } + + const formatDate = (dateString: string) => { + const date = new Date(dateString) + return date.toLocaleString() + } + + const isExpired = (dateString: string) => { + return new Date(dateString) < new Date() + } + + return ( +
+ {/* Header */} +
+

+ Welcome back, {user?.username}! +

+

+ Here's an overview of your todos and settings. +

+
+ + {/* Stats Grid */} +
+ {stats.map((stat) => { + const Icon = stat.icon + return ( +
+
+
+
+ +
+
+
+
+
+ {stat.name} +
+
+ {stat.value} +
+
+
+
+
+ ) + })} +
+ + {/* Overdue Reminders Alert */} + {overdueReminders.length > 0 && ( +
+
+ +

+ You have {overdueReminders.length} overdue reminder{overdueReminders.length > 1 ? 's' : ''} +

+
+
+ {overdueReminders.slice(0, 3).map((reminder: any) => ( +
+

{reminder.message}

+ +
+ ))} + {overdueReminders.length > 3 && ( +

And {overdueReminders.length - 3} more...

+ )} +
+
+ )} + + {/* Recent Activity */} +
+ {/* Recent Todos */} +
+
+

Recent Todos

+ + View all + +
+ {recentTodos.length > 0 ? ( +
+ {recentTodos.map((todo: any) => ( +
+
+ + {todo.item} + +
+ ))} +
+ ) : ( +

No todos yet. Create your first one!

+ )} +
+ + {/* Recent Reminders */} +
+
+

Recent Reminders

+ + View all + +
+ {recentReminders.length > 0 ? ( +
+ {recentReminders.map((reminder: any) => ( +
+
+
+

+ {reminder.message} +

+

+ {formatDate(reminder.expires_at)} +

+
+
+ ))} +
+ ) : ( +

No reminders yet. Create your first one!

+ )} +
+ + {/* API Key Status */} +
+
+

AI Configuration

+ + Manage + +
+
+
+ API Key + + {hasApiKey ? 'Configured' : 'Not Set'} + +
+ {apiKeyInfo?.model && ( +
+ Model + + {apiKeyInfo.model} + +
+ )} + {apiKeyInfo?.apiUrl && ( +
+ Endpoint + + {new URL(apiKeyInfo.apiUrl).hostname} + +
+ )} + {!hasApiKey && ( +

+ Configure your AI API key to use custom models and endpoints. +

+ )} +
+
+
+ + {/* Quick Actions */} + +
+ ) +} + +export default DashboardPage \ No newline at end of file diff --git a/web/src/pages/LandingPage.tsx b/web/src/pages/LandingPage.tsx new file mode 100644 index 0000000..c53a266 --- /dev/null +++ b/web/src/pages/LandingPage.tsx @@ -0,0 +1,156 @@ +import { Bot, MessageSquare, Cloud, Bell, Shield, Zap } from 'lucide-react'; +import { Link } from 'react-router-dom'; + +const LandingPage = () => { + return ( +
+ {/* Header */} +
+
+
+
+ Aethel +
+ + + + + Dashboard + +
+
+
+ + {/* Hero Section */} +
+
+

+ A useful Discord user bot + for your account +

+ +

+ Enhance your Discord experience with AI chat, weather updates, reminders, and more useful features. +

+ + + + + + Add to Discord + +
+
+ + {/* Features Section */} +
+
+
+

Features

+

+ Everything you need to enhance your Discord experience +

+
+ +
+
+
+ +
+

AI Chat Assistant

+

+ Get intelligent responses and assistance with our advanced AI chat system. +

+
+ +
+
+ +
+

Weather Updates

+

+ Stay informed with real-time weather information for any location. +

+
+ +
+
+ +
+

Smart Reminders

+

+ Never miss important events with our intelligent reminder system. +

+
+ +
+
+ +
+

Secure & Private

+

+ Your data is protected with enterprise-grade security measures. +

+
+ +
+
+ +
+

Lightning Fast

+

+ Experience blazing fast response times and seamless performance. +

+
+ +
+
+ +
+

Discord Native

+

+ Built specifically for Discord with seamless integration. +

+
+
+
+
+ + {/* Footer */} +
+
+
+ Aethel +
+

+ A useful Discord user bot for your account +

+ +
+
+
+ ) +} + +export default LandingPage \ No newline at end of file diff --git a/web/src/pages/LoginPage.tsx b/web/src/pages/LoginPage.tsx new file mode 100644 index 0000000..a0b82fb --- /dev/null +++ b/web/src/pages/LoginPage.tsx @@ -0,0 +1,84 @@ +import { useEffect } from 'react' +import { useSearchParams } from 'react-router-dom' +import { toast } from 'sonner' +import { useAuthStore } from '../stores/authStore' + +const LoginPage = () => { + const [searchParams] = useSearchParams() + const { login } = useAuthStore() + + useEffect(() => { + const token = searchParams.get('token') + const error = searchParams.get('error') + + if (error) { + toast.error('Authentication failed. Please try again.') + } else if (token) { + const userData = { + id: searchParams.get('user_id') || '', + username: searchParams.get('username') || '', + discriminator: searchParams.get('discriminator') || '', + avatar: searchParams.get('avatar'), + } + + login(token, userData) + toast.success('Successfully logged in!') + } + }, [searchParams, login]) + + const handleDiscordLogin = async () => { + try { + window.location.href = '/api/auth/discord' + } catch (error) { + toast.error('Failed to initiate Discord login') + } + } + + return ( +
+
+
+

Aethel Dashboard

+

+ Manage your todos and AI API keys +

+
+ +
+
+
+

+ Welcome back +

+

+ Sign in with your Discord account to continue +

+
+ + + +
+ By signing in, you agree to our terms of service and privacy policy. +
+
+
+ +
+

+ Need help? Contact us on our Discord server +

+
+
+
+ ) +} + +export default LoginPage \ No newline at end of file diff --git a/web/src/pages/PrivacyPage.tsx b/web/src/pages/PrivacyPage.tsx new file mode 100644 index 0000000..a99f010 --- /dev/null +++ b/web/src/pages/PrivacyPage.tsx @@ -0,0 +1,114 @@ +import { Link } from 'react-router-dom'; +import { ArrowLeft } from 'lucide-react'; + +export default function PrivacyPolicy() { + return ( +
+ {/* Header */} +
+
+ + + Back to Home + +
+
+ + {/* Content */} +
+
+

Privacy Policy

+

Last Updated: July 21, 2025

+
+
+
+

1. Information We Collect

+

+ The bot ("the Bot") collects the following information: +

+
    +
  • Discord user IDs for command processing and functionality
  • +
  • Server IDs where the Bot is used
  • +
  • Channel IDs where commands are used
  • +
  • Message content for commands that require it (e.g., reminders, AI chat)
  • +
  • API keys provided by users (encrypted and stored safely on our database until the user says otherwise)
  • +
  • Commands ran and users who ran them
  • + +
+
+ +
+

2. How We Use Your Information

+

+ We use the collected information to provide, maintain, and improve our Bot's services, including: +

+
    +
  • Provide and maintain the Bot's functionality
  • +
  • Process commands and provide responses
  • +
  • Improve the Bot's performance and features
  • +
  • Monitor for abuse and prevent violations of our Terms of Service
  • +
+
+ +
+

3. Data Storage

+

+ We take your privacy seriously: +

+
    +
  • API keys are securely hashed using industry-standard encryption before being stored in our database
  • +
  • Your custom API keys and model preferences are stored until you choose to remove them using the /ai use_custom_api:false command
  • +
  • We log all message content (like Wiki searches, reminders, and 8-ball queries) for monitoring purposes.
  • +
  • We do not sell or share your personal information with third parties
  • +
  • You can delete your stored API key and preferences at any time by running /ai use_custom_api:false
  • +
+
+ +
+

4. Third-Party Services

+

+ Our Bot may contain links to third-party websites or services that are not operated by us. We have no control over and assume no responsibility for the content, privacy policies, or practices of any third-party sites or services. +

+
    +
  • Discord's Privacy Policy for user and server data
  • +
  • OpenRouter's Privacy Policy for AI chat functionality
  • +
  • Weather API providers for weather information
  • +
  • Wikipedia's Terms of Service for wiki lookups
  • +
+
+ +
+

5. Data Security

+

+ We implement reasonable security measures to protect your information, but no method of transmission over the internet is 100% secure. +

+
+ +
+

6. Children's Privacy

+

+ Our Bot is not intended for use by children under the age of 13. We do not knowingly collect personally identifiable information from children under 13. If you are a parent or guardian and you are aware that your child has provided us with personal information, please contact us. +

+
+ +
+

7. Changes to This Policy

+

+ We may update our Privacy Policy from time to time. We will notify you of any changes by posting the new Privacy Policy on this page. +

+
+ +
+

8. Contact Us

+

+ If you have any questions about this Privacy Policy, please contact us at scan@scanash.com. +

+
+
+
+
+ ); +} \ No newline at end of file diff --git a/web/src/pages/RemindersPage.tsx b/web/src/pages/RemindersPage.tsx new file mode 100644 index 0000000..294e85f --- /dev/null +++ b/web/src/pages/RemindersPage.tsx @@ -0,0 +1,262 @@ +import React, { useState, useEffect } from 'react' +import { Plus, Clock, Check, Trash2, Bell } from 'lucide-react' +import { remindersAPI } from '../lib/api' +import { toast } from 'sonner' + +interface Reminder { + reminder_id: string + message: string + expires_at: string + is_completed: boolean + created_at: string +} + +const RemindersPage: React.FC = () => { + const [reminders, setReminders] = useState([]) + const [loading, setLoading] = useState(true) + const [showCreateForm, setShowCreateForm] = useState(false) + const [newReminder, setNewReminder] = useState({ + message: '', + expires_at: '' + }) + + useEffect(() => { + fetchReminders() + }, []) + + const fetchReminders = async () => { + try { + const response = await remindersAPI.getReminders() + setReminders(response.data.reminders || []) + } catch (error) { + toast.error('Failed to fetch reminders') + } finally { + setLoading(false) + } + } + + const handleCreateReminder = async (e: React.FormEvent) => { + e.preventDefault() + + if (!newReminder.message.trim() || !newReminder.expires_at) { + toast.error('Please fill in all fields') + return + } + + try { + await remindersAPI.createReminder({ + message: newReminder.message.trim(), + expires_at: newReminder.expires_at + }) + + toast.success('Reminder created successfully!') + setNewReminder({ message: '', expires_at: '' }) + setShowCreateForm(false) + fetchReminders() + } catch (error) { + toast.error('Failed to create reminder') + } + } + + const handleCompleteReminder = async (id: string) => { + try { + await remindersAPI.completeReminder(id) + toast.success('Reminder completed!') + fetchReminders() + } catch (error) { + toast.error('Failed to complete reminder') + } + } + + const handleClearCompleted = async () => { + try { + const completedReminders = reminders.filter(r => r.is_completed) + if (completedReminders.length === 0) { + toast.info('No completed reminders to clear') + return + } + + await remindersAPI.clearCompletedReminders() + toast.success('Completed reminders cleared!') + fetchReminders() + } catch (error) { + toast.error('Failed to clear completed reminders') + } + } + + const formatDate = (dateString: string) => { + const date = new Date(dateString) + return date.toLocaleString() + } + + const isExpired = (dateString: string) => { + return new Date(dateString) < new Date() + } + + const getMinDateTime = () => { + const now = new Date() + const minTime = new Date(now.getTime() + 60000) + const year = minTime.getFullYear() + const month = String(minTime.getMonth() + 1).padStart(2, '0') + const day = String(minTime.getDate()).padStart(2, '0') + const hours = String(minTime.getHours()).padStart(2, '0') + const minutes = String(minTime.getMinutes()).padStart(2, '0') + + return `${year}-${month}-${day}T${hours}:${minutes}` + } + + if (loading) { + return ( +
+
+
+ ) + } + + return ( +
+
+
+

Reminders

+

Manage your personal reminders and notifications

+
+
+ {reminders.some(r => r.is_completed) && ( + + )} + +
+
+ + {/* Create Reminder Form */} + {showCreateForm && ( +
+

Create New Reminder

+
+
+ +