diff --git a/backend/jest.config.js b/backend/jest.config.js index 6834ca81..c2f9e3e5 100644 --- a/backend/jest.config.js +++ b/backend/jest.config.js @@ -2,14 +2,17 @@ process.env.NODE_ENV = 'test'; export default { - setupFiles: ['dotenv/config', '/jest.setup.js'], + setupFiles: [ + 'dotenv/config', + '/jest.setup.js', + '/tests/jest.setup.ts', + ], preset: 'ts-jest', testEnvironment: 'node', extensionsToTreatAsEsm: ['.ts'], moduleNameMapper: { '^(\\.{1,2}/.*)\\.js$': '$1', }, - setupFiles: ['/tests/jest.setup.ts'], transform: { '^.+\\.tsx?$': [ 'ts-jest', diff --git a/backend/package.json b/backend/package.json index 9d26d9b6..a3695fdd 100644 --- a/backend/package.json +++ b/backend/package.json @@ -37,10 +37,12 @@ "ioredis": "^5.10.1", "json2csv": "^6.0.0-alpha.2", "jsonwebtoken": "^9.0.2", + "marked": "^9.1.6", "openai": "^6.32.0", "pg": "^8.20.0", "prisma": "^7.8.0", "qrcode": "^1.5.4", + "sanitize-html": "^2.17.5", "socket.io": "^4.8.3", "swagger-jsdoc": "^6.3.0", "swagger-ui-express": "^5.0.1", @@ -58,6 +60,7 @@ "@types/jsonwebtoken": "^9.0.10", "@types/node": "^25.5.0", "@types/qrcode": "^1.5.5", + "@types/sanitize-html": "^2.16.1", "@types/supertest": "^7.2.0", "@types/swagger-jsdoc": "^6.0.4", "@types/swagger-ui-express": "^4.1.8", diff --git a/backend/pnpm-lock.yaml b/backend/pnpm-lock.yaml index 5f5208ac..227b5d07 100644 --- a/backend/pnpm-lock.yaml +++ b/backend/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@apollo/server': + specifier: ^5.5.1 + version: 5.5.1(graphql@16.14.2) '@prisma/adapter-pg': specifier: ^7.8.0 version: 7.8.0 @@ -38,6 +41,9 @@ importers: cors: specifier: ^2.8.6 version: 2.8.6 + dataloader: + specifier: ^2.2.3 + version: 2.2.3 dotenv: specifier: ^17.3.1 version: 17.4.2 @@ -50,6 +56,12 @@ importers: express-rate-limit: specifier: ^8.3.1 version: 8.5.2(express@5.2.1) + graphql: + specifier: ^16.14.0 + version: 16.14.2 + graphql-depth-limit: + specifier: ^1.1.0 + version: 1.1.0(graphql@16.14.2) ioredis: specifier: ^5.10.1 version: 5.11.0 @@ -59,6 +71,9 @@ importers: jsonwebtoken: specifier: ^9.0.2 version: 9.0.3 + marked: + specifier: ^9.1.6 + version: 9.1.6 openai: specifier: ^6.32.0 version: 6.39.1(ws@8.21.0)(zod@4.4.3) @@ -71,6 +86,9 @@ importers: qrcode: specifier: ^1.5.4 version: 1.5.4 + sanitize-html: + specifier: ^2.17.5 + version: 2.17.5 socket.io: specifier: ^4.8.3 version: 4.8.3 @@ -108,12 +126,18 @@ importers: '@types/jest': specifier: ^30.0.0 version: 30.0.0 + '@types/jsonwebtoken': + specifier: ^9.0.10 + version: 9.0.10 '@types/node': specifier: ^25.5.0 version: 25.9.1 '@types/qrcode': specifier: ^1.5.5 version: 1.5.6 + '@types/sanitize-html': + specifier: ^2.16.1 + version: 2.16.1 '@types/supertest': specifier: ^7.2.0 version: 7.2.0 @@ -166,6 +190,89 @@ packages: peerDependencies: openapi-types: '>=7' + '@apollo/cache-control-types@1.0.3': + resolution: {integrity: sha512-F17/vCp7QVwom9eG7ToauIKdAxpSoadsJnqIfyryLFSkLSOEqu+eC5Z3N8OXcUVStuOMcNHlyraRsA6rRICu4g==} + peerDependencies: + graphql: 14.x || 15.x || 16.x + + '@apollo/protobufjs@1.2.8': + resolution: {integrity: sha512-r7xNeUqZX+eBBEmyvaPw0/cSz6zgf5jdH8mjUz8ynKpNs/GU7vi2T7sNcZINk2ZID7wwjG91FCgdpCrQuJ8rzA==} + hasBin: true + + '@apollo/server-gateway-interface@2.0.0': + resolution: {integrity: sha512-3HEMD6fSantG2My3jWkb9dvfkF9vJ4BDLRjMgsnD790VINtuPaEp+h3Hg9HOHiWkML6QsOhnaRqZ+gvhp3y8Nw==} + peerDependencies: + graphql: 14.x || 15.x || 16.x + + '@apollo/server@5.5.1': + resolution: {integrity: sha512-Rn3g5TJQsMSUY23CWZTghWdBWyjX7dP1eaEBPkvmM2RHi82cDcpgTIkSCbGvtTUEGjwopLv1AAooU/n7iIZ20A==} + engines: {node: '>=20'} + peerDependencies: + graphql: ^16.11.0 + + '@apollo/usage-reporting-protobuf@4.1.2': + resolution: {integrity: sha512-aTnAD41RYz0d5dawlyR5Iclkgzx0Xb0njUJmEfvZ6pS4f4HU8wCYyctPpWat/HWp2PmRwDfX5R1k4uVcDKZ4xA==} + + '@apollo/utils.createhash@3.0.1': + resolution: {integrity: sha512-CKrlySj4eQYftBE5MJ8IzKwIibQnftDT7yGfsJy5KSEEnLlPASX0UTpbKqkjlVEwPPd4mEwI7WOM7XNxEuO05A==} + engines: {node: '>=16'} + + '@apollo/utils.dropunuseddefinitions@2.0.1': + resolution: {integrity: sha512-EsPIBqsSt2BwDsv8Wu76LK5R1KtsVkNoO4b0M5aK0hx+dGg9xJXuqlr7Fo34Dl+y83jmzn+UvEW+t1/GP2melA==} + engines: {node: '>=14'} + peerDependencies: + graphql: 14.x || 15.x || 16.x + + '@apollo/utils.fetcher@3.1.0': + resolution: {integrity: sha512-Z3QAyrsQkvrdTuHAFwWDNd+0l50guwoQUoaDQssLOjkmnmVuvXlJykqlEJolio+4rFwBnWdoY1ByFdKaQEcm7A==} + engines: {node: '>=16'} + + '@apollo/utils.isnodelike@3.0.0': + resolution: {integrity: sha512-xrjyjfkzunZ0DeF6xkHaK5IKR8F1FBq6qV+uZ+h9worIF/2YSzA0uoBxGv6tbTeo9QoIQnRW4PVFzGix5E7n/g==} + engines: {node: '>=16'} + + '@apollo/utils.keyvaluecache@4.0.0': + resolution: {integrity: sha512-mKw1myRUkQsGPNB+9bglAuhviodJ2L2MRYLTafCMw5BIo7nbvCPNCkLnIHjZ1NOzH7SnMAr5c9LmXiqsgYqLZw==} + engines: {node: '>=20'} + + '@apollo/utils.logger@3.0.0': + resolution: {integrity: sha512-M8V8JOTH0F2qEi+ktPfw4RL7MvUycDfKp7aEap2eWXfL5SqWHN6jTLbj5f5fj1cceHpyaUSOZlvlaaryaxZAmg==} + engines: {node: '>=16'} + + '@apollo/utils.printwithreducedwhitespace@2.0.1': + resolution: {integrity: sha512-9M4LUXV/fQBh8vZWlLvb/HyyhjJ77/I5ZKu+NBWV/BmYGyRmoEP9EVAy7LCVoY3t8BDcyCAGfxJaLFCSuQkPUg==} + engines: {node: '>=14'} + peerDependencies: + graphql: 14.x || 15.x || 16.x + + '@apollo/utils.removealiases@2.0.1': + resolution: {integrity: sha512-0joRc2HBO4u594Op1nev+mUF6yRnxoUH64xw8x3bX7n8QBDYdeYgY4tF0vJReTy+zdn2xv6fMsquATSgC722FA==} + engines: {node: '>=14'} + peerDependencies: + graphql: 14.x || 15.x || 16.x + + '@apollo/utils.sortast@2.0.1': + resolution: {integrity: sha512-eciIavsWpJ09za1pn37wpsCGrQNXUhM0TktnZmHwO+Zy9O4fu/WdB4+5BvVhFiZYOXvfjzJUcc+hsIV8RUOtMw==} + engines: {node: '>=14'} + peerDependencies: + graphql: 14.x || 15.x || 16.x + + '@apollo/utils.stripsensitiveliterals@2.0.1': + resolution: {integrity: sha512-QJs7HtzXS/JIPMKWimFnUMK7VjkGQTzqD9bKD1h3iuPAqLsxd0mUNVbkYOPTsDhUKgcvUOfOqOJWYohAKMvcSA==} + engines: {node: '>=14'} + peerDependencies: + graphql: 14.x || 15.x || 16.x + + '@apollo/utils.usagereporting@2.1.0': + resolution: {integrity: sha512-LPSlBrn+S17oBy5eWkrRSGb98sWmnEzo3DPTZgp8IQc8sJe0prDgDuppGq4NeQlpoqEHz0hQeYHAOA0Z3aQsxQ==} + engines: {node: '>=14'} + peerDependencies: + graphql: 14.x || 15.x || 16.x + + '@apollo/utils.withrequired@3.0.0': + resolution: {integrity: sha512-aaxeavfJ+RHboh7c2ofO5HHtQobGX4AgUujXP4CXpREHp9fQ9jPi6K9T1jrAKe7HIipoP0OJ1gd6JamSkFIpvA==} + engines: {node: '>=16'} + '@babel/code-frame@7.29.7': resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} engines: {node: '>=6.9.0'} @@ -611,6 +718,29 @@ packages: '@ethersproject/wordlists@5.8.0': resolution: {integrity: sha512-2df9bbXicZws2Sb5S6ET493uJ0Z84Fjr3pC4tu/qlnZERibZCeUVuqdtt+7Tv9xxhUxHoIekIA7avrKUWHrezg==} + '@graphql-tools/merge@9.1.9': + resolution: {integrity: sha512-iHUWNjRHeQRYdgIMIuChThOwoKzA9vrzYeslgfBo5eUYEyHGZCoDPjAavssoYXLwstYt1dZj2J22jSzc2DrN0Q==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/schema@10.0.33': + resolution: {integrity: sha512-O6P3RIftO0jafnSsFAqpjurUuUxJ43s/AdPVLQsBkI6y4Ic/tKm4C1Qm1KKQsCDTOxXPJClh/v3g7k7yLKCFBQ==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/utils@11.1.0': + resolution: {integrity: sha512-PtFVG4r8Z2LEBSaPYQMusBiB3o6kjLVJyjCLbnWem/SpSuM21v6LTmgpkXfYU1qpBV2UGsFyuEnSJInl8fR1Ag==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-typed-document-node/core@3.2.0': + resolution: {integrity: sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + '@hono/node-server@1.19.11': resolution: {integrity: sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==} engines: {node: '>=18.14.1'} @@ -1091,6 +1221,36 @@ packages: react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.5': + resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==} + + '@protobufjs/eventemitter@1.1.1': + resolution: {integrity: sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==} + + '@protobufjs/fetch@1.1.1': + resolution: {integrity: sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/inquire@1.1.2': + resolution: {integrity: sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.1': + resolution: {integrity: sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==} + '@radix-ui/primitive@1.1.3': resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} @@ -1300,9 +1460,18 @@ packages: '@types/json2csv@5.0.7': resolution: {integrity: sha512-Ma25zw9G9GEBnX8b12R4EYvnFT6dBh8L3jwsN5EUFXa+fl2dqmbLDbNWN0XuQU3rSXdsbBeCYjI9uHU2PUBxhA==} + '@types/jsonwebtoken@9.0.10': + resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} + + '@types/long@4.0.2': + resolution: {integrity: sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==} + '@types/methods@1.1.4': resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/mysql@2.15.26': resolution: {integrity: sha512-DSLCOXhkvfS5WNNPbfn2KdICAmk8lLc+/PNvnPnF7gOdMZCxopXduqv0OQ13y/yA/zXTSikZZqVgybUxOEg6YQ==} @@ -1330,6 +1499,9 @@ packages: '@types/react@19.2.15': resolution: {integrity: sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==} + '@types/sanitize-html@2.16.1': + resolution: {integrity: sha512-n9wjs8bCOTyN/ynwD8s/nTcTreIHB1vf31vhLMGqUPNHaweKC4/fAl4Dj+hUlCTKYgm4P3k83fmiFfzkZ6sgMA==} + '@types/send@1.2.1': resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} @@ -1492,6 +1664,10 @@ packages: cpu: [x64] os: [win32] + '@whatwg-node/promise-helpers@1.3.2': + resolution: {integrity: sha512-Nst5JdK47VIl9UcGwtv2Rcgyn5lWtZ0/mhRQ4G8NN2isxpq2TO30iqHzmwoJycjWuyUfg3GFXqP/gFHXeV57IA==} + engines: {node: '>=16.0.0'} + accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -1569,9 +1745,16 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + arrify@1.0.1: + resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==} + engines: {node: '>=0.10.0'} + asap@2.0.6: resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + async-retry@1.3.3: + resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==} + async@3.2.6: resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} @@ -1878,6 +2061,10 @@ packages: resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} engines: {node: '>=12.0.0'} + cross-inspect@1.0.1: + resolution: {integrity: sha512-Pcw1JTvZLSJH83iiGWt6fRcT+BjZlCDRVwYLbUcHzv/CRpB7r0MlSrGbIyQvVSNyGnbt7G4AXuyCiDR3POvZ1A==} + engines: {node: '>=16.0.0'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -1885,6 +2072,12 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + dataloader@2.2.3: + resolution: {integrity: sha512-y2krtASINtPFS1rSDjacrFgn1dcUuoREVabwlOGOe4SdxenREqwjwjElAdwvbGM7kgZz9a3KVicWR7vcz8rnzA==} + + dayjs@1.11.21: + resolution: {integrity: sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -1958,6 +2151,19 @@ packages: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} engines: {node: '>=6.0.0'} + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + dotenv@17.4.2: resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} engines: {node: '>=12'} @@ -2013,6 +2219,14 @@ packages: resolution: {integrity: sha512-2agL3ueZhqxoVrfmntO8yuVj+uNSlIOnhykYHk3Cq0ShYPdUjjUiSJrQvXjq01I9jAuI0Zl2YO8Evv5Mqytm5g==} engines: {node: '>=10.2.0'} + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + env-paths@3.0.0: resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -2052,6 +2266,10 @@ packages: resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} engines: {node: '>=8'} + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} @@ -2250,6 +2468,16 @@ packages: graphmatch@1.1.1: resolution: {integrity: sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg==} + graphql-depth-limit@1.1.0: + resolution: {integrity: sha512-+3B2BaG8qQ8E18kzk9yiSdAa75i/hnnOwgSeAxVJctGQPvmeiLtqKOYF6HETCyRjiF7Xfsyal0HbLlxCQkgkrw==} + engines: {node: '>=6.0.0'} + peerDependencies: + graphql: '*' + + graphql@16.14.2: + resolution: {integrity: sha512-Chq1s4CY7jmh8gO2qvLIJyfCDIN+EHLFW/9iShnp1z8FjBQMoodWP1kDC36VAMXXIvAjj4ARa7ntfAV2BrjsbA==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + handlebars@4.7.9: resolution: {integrity: sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==} engines: {node: '>=0.4.7'} @@ -2287,6 +2515,9 @@ packages: html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + htmlparser2@10.1.0: + resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==} + http-errors@2.0.1: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} @@ -2370,6 +2601,10 @@ packages: resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} engines: {node: '>=6'} + is-plain-object@5.0.0: + resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} + engines: {node: '>=0.10.0'} + is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} @@ -2604,6 +2839,9 @@ packages: kuler@2.0.0: resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} + launder@1.7.1: + resolution: {integrity: sha512-mU6WRz5EusL9ZZuiZ5SO4Y6C0P9PAUR9iwdb6bzj4KDihm28DiHFw+/yk9DBH4f+Pv1wuzQ4e2jV3oQ7mkIqvw==} + leven@3.1.0: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} engines: {node: '>=6'} @@ -2657,10 +2895,20 @@ packages: lodash.once@4.1.1: resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + lodash.sortby@4.7.0: + resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} + logform@2.7.0: resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==} engines: {node: '>= 12.0.0'} + loglevel@1.9.2: + resolution: {integrity: sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==} + engines: {node: '>= 0.6.0'} + + long@4.0.0: + resolution: {integrity: sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==} + long@5.3.2: resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} @@ -2692,6 +2940,11 @@ packages: makeerror@1.0.12: resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + marked@9.1.6: + resolution: {integrity: sha512-jcByLnIFkd5gSXZmjNvS1TlmRhCXZjIzHYlaGkPlLIekG55JDR2Z4va9tZwCiP+/RDERiNhMOFu01xd6O5ct1Q==} + engines: {node: '>= 16'} + hasBin: true + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -2781,6 +3034,11 @@ packages: resolution: {integrity: sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==} engines: {node: '>=8.0.0'} + nanoid@3.3.15: + resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + napi-postinstall@0.3.4: resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} @@ -2885,6 +3143,9 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} + parse-srcset@1.0.2: + resolution: {integrity: sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==} + parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -2985,6 +3246,10 @@ packages: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + engines: {node: ^10 || ^12 || >=14} + postgres-array@2.0.0: resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} engines: {node: '>=4'} @@ -3136,6 +3401,10 @@ packages: resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} engines: {node: '>= 4'} + retry@0.13.1: + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} + engines: {node: '>= 4'} + router@2.2.0: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} engines: {node: '>= 18'} @@ -3150,6 +3419,9 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + sanitize-html@2.17.5: + resolution: {integrity: sha512-ZmU1joGRrvoyctKIiuwUxqR6moLoU2Wk+2bMccN6f7UwhAmwYDvWziqPxRDDN2Qip62NqnIrVrT9akbL6Wretg==} + scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -3245,6 +3517,10 @@ packages: resolution: {integrity: sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==} engines: {node: '>=10.2.0'} + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + source-map-support@0.5.13: resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} @@ -3508,6 +3784,10 @@ packages: walker@1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + which-module@2.0.1: resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} @@ -3671,6 +3951,112 @@ snapshots: call-me-maybe: 1.0.2 openapi-types: 12.1.3 + '@apollo/cache-control-types@1.0.3(graphql@16.14.2)': + dependencies: + graphql: 16.14.2 + + '@apollo/protobufjs@1.2.8': + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.5 + '@protobufjs/eventemitter': 1.1.1 + '@protobufjs/fetch': 1.1.1 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.2 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.1 + '@types/long': 4.0.2 + long: 4.0.0 + + '@apollo/server-gateway-interface@2.0.0(graphql@16.14.2)': + dependencies: + '@apollo/usage-reporting-protobuf': 4.1.2 + '@apollo/utils.fetcher': 3.1.0 + '@apollo/utils.keyvaluecache': 4.0.0 + '@apollo/utils.logger': 3.0.0 + graphql: 16.14.2 + + '@apollo/server@5.5.1(graphql@16.14.2)': + dependencies: + '@apollo/cache-control-types': 1.0.3(graphql@16.14.2) + '@apollo/server-gateway-interface': 2.0.0(graphql@16.14.2) + '@apollo/usage-reporting-protobuf': 4.1.2 + '@apollo/utils.createhash': 3.0.1 + '@apollo/utils.fetcher': 3.1.0 + '@apollo/utils.isnodelike': 3.0.0 + '@apollo/utils.keyvaluecache': 4.0.0 + '@apollo/utils.logger': 3.0.0 + '@apollo/utils.usagereporting': 2.1.0(graphql@16.14.2) + '@apollo/utils.withrequired': 3.0.0 + '@graphql-tools/schema': 10.0.33(graphql@16.14.2) + async-retry: 1.3.3 + body-parser: 2.2.2 + content-type: 1.0.5 + cors: 2.8.6 + finalhandler: 2.1.1 + graphql: 16.14.2 + loglevel: 1.9.2 + lru-cache: 11.5.1 + negotiator: 1.0.0 + whatwg-mimetype: 4.0.0 + transitivePeerDependencies: + - supports-color + + '@apollo/usage-reporting-protobuf@4.1.2': + dependencies: + '@apollo/protobufjs': 1.2.8 + + '@apollo/utils.createhash@3.0.1': + dependencies: + '@apollo/utils.isnodelike': 3.0.0 + sha.js: 2.4.12 + + '@apollo/utils.dropunuseddefinitions@2.0.1(graphql@16.14.2)': + dependencies: + graphql: 16.14.2 + + '@apollo/utils.fetcher@3.1.0': {} + + '@apollo/utils.isnodelike@3.0.0': {} + + '@apollo/utils.keyvaluecache@4.0.0': + dependencies: + '@apollo/utils.logger': 3.0.0 + lru-cache: 11.5.1 + + '@apollo/utils.logger@3.0.0': {} + + '@apollo/utils.printwithreducedwhitespace@2.0.1(graphql@16.14.2)': + dependencies: + graphql: 16.14.2 + + '@apollo/utils.removealiases@2.0.1(graphql@16.14.2)': + dependencies: + graphql: 16.14.2 + + '@apollo/utils.sortast@2.0.1(graphql@16.14.2)': + dependencies: + graphql: 16.14.2 + lodash.sortby: 4.7.0 + + '@apollo/utils.stripsensitiveliterals@2.0.1(graphql@16.14.2)': + dependencies: + graphql: 16.14.2 + + '@apollo/utils.usagereporting@2.1.0(graphql@16.14.2)': + dependencies: + '@apollo/usage-reporting-protobuf': 4.1.2 + '@apollo/utils.dropunuseddefinitions': 2.0.1(graphql@16.14.2) + '@apollo/utils.printwithreducedwhitespace': 2.0.1(graphql@16.14.2) + '@apollo/utils.removealiases': 2.0.1(graphql@16.14.2) + '@apollo/utils.sortast': 2.0.1(graphql@16.14.2) + '@apollo/utils.stripsensitiveliterals': 2.0.1(graphql@16.14.2) + graphql: 16.14.2 + + '@apollo/utils.withrequired@3.0.0': {} + '@babel/code-frame@7.29.7': dependencies: '@babel/helper-validator-identifier': 7.29.7 @@ -4231,6 +4617,31 @@ snapshots: '@ethersproject/properties': 5.8.0 '@ethersproject/strings': 5.8.0 + '@graphql-tools/merge@9.1.9(graphql@16.14.2)': + dependencies: + '@graphql-tools/utils': 11.1.0(graphql@16.14.2) + graphql: 16.14.2 + tslib: 2.8.1 + + '@graphql-tools/schema@10.0.33(graphql@16.14.2)': + dependencies: + '@graphql-tools/merge': 9.1.9(graphql@16.14.2) + '@graphql-tools/utils': 11.1.0(graphql@16.14.2) + graphql: 16.14.2 + tslib: 2.8.1 + + '@graphql-tools/utils@11.1.0(graphql@16.14.2)': + dependencies: + '@graphql-typed-document-node/core': 3.2.0(graphql@16.14.2) + '@whatwg-node/promise-helpers': 1.3.2 + cross-inspect: 1.0.1 + graphql: 16.14.2 + tslib: 2.8.1 + + '@graphql-typed-document-node/core@3.2.0(graphql@16.14.2)': + dependencies: + graphql: 16.14.2 + '@hono/node-server@1.19.11(hono@4.12.23)': dependencies: hono: 4.12.23 @@ -4906,6 +5317,28 @@ snapshots: transitivePeerDependencies: - '@types/react-dom' + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.5': {} + + '@protobufjs/eventemitter@1.1.1': {} + + '@protobufjs/fetch@1.1.1': + dependencies: + '@protobufjs/aspromise': 1.1.2 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/inquire@1.1.2': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.1': {} + '@radix-ui/primitive@1.1.3': {} '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.15)(react@19.2.6)': @@ -5160,8 +5593,17 @@ snapshots: dependencies: '@types/node': 25.9.1 + '@types/jsonwebtoken@9.0.10': + dependencies: + '@types/ms': 2.1.0 + '@types/node': 25.9.1 + + '@types/long@4.0.2': {} + '@types/methods@1.1.4': {} + '@types/ms@2.1.0': {} + '@types/mysql@2.15.26': dependencies: '@types/node': 25.9.1 @@ -5198,6 +5640,10 @@ snapshots: dependencies: csstype: 3.2.3 + '@types/sanitize-html@2.16.1': + dependencies: + htmlparser2: 10.1.0 + '@types/send@1.2.1': dependencies: '@types/node': 25.9.1 @@ -5318,6 +5764,10 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.12.2': optional: true + '@whatwg-node/promise-helpers@1.3.2': + dependencies: + tslib: 2.8.1 + accepts@1.3.8: dependencies: mime-types: 2.1.35 @@ -5386,8 +5836,14 @@ snapshots: argparse@2.0.1: {} + arrify@1.0.1: {} + asap@2.0.6: {} + async-retry@1.3.3: + dependencies: + retry: 0.13.1 + async@3.2.6: {} asynckit@0.4.0: {} @@ -5696,6 +6152,10 @@ snapshots: dependencies: luxon: 3.7.2 + cross-inspect@1.0.1: + dependencies: + tslib: 2.8.1 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -5704,6 +6164,10 @@ snapshots: csstype@3.2.3: {} + dataloader@2.2.3: {} + + dayjs@1.11.21: {} + debug@4.4.3: dependencies: ms: 2.1.3 @@ -5750,6 +6214,24 @@ snapshots: dependencies: esutils: 2.0.3 + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + dotenv@17.4.2: {} dunder-proto@1.0.1: @@ -5814,6 +6296,10 @@ snapshots: - supports-color - utf-8-validate + entities@4.5.0: {} + + entities@7.0.1: {} + env-paths@3.0.0: {} error-ex@1.3.4: @@ -5870,6 +6356,8 @@ snapshots: escape-string-regexp@2.0.0: {} + escape-string-regexp@4.0.0: {} + esprima@4.0.1: {} esutils@2.0.3: {} @@ -6134,6 +6622,13 @@ snapshots: graphmatch@1.1.1: {} + graphql-depth-limit@1.1.0(graphql@16.14.2): + dependencies: + arrify: 1.0.1 + graphql: 16.14.2 + + graphql@16.14.2: {} + handlebars@4.7.9: dependencies: minimist: 1.2.8 @@ -6174,6 +6669,13 @@ snapshots: html-escaper@2.0.2: {} + htmlparser2@10.1.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 7.0.1 + http-errors@2.0.1: dependencies: depd: 2.0.0 @@ -6272,6 +6774,8 @@ snapshots: is-generator-fn@2.1.0: {} + is-plain-object@5.0.0: {} + is-promise@4.0.0: {} is-property@1.0.2: {} @@ -6698,6 +7202,10 @@ snapshots: kuler@2.0.0: {} + launder@1.7.1: + dependencies: + dayjs: 1.11.21 + leven@3.1.0: {} lib0@0.2.117: @@ -6734,6 +7242,8 @@ snapshots: lodash.once@4.1.1: {} + lodash.sortby@4.7.0: {} + logform@2.7.0: dependencies: '@colors/colors': 1.6.0 @@ -6743,6 +7253,10 @@ snapshots: safe-stable-stringify: 2.5.0 triple-beam: 1.4.1 + loglevel@1.9.2: {} + + long@4.0.0: {} + long@5.3.2: {} lru-cache@10.4.3: {} @@ -6767,6 +7281,8 @@ snapshots: dependencies: tmpl: 1.0.5 + marked@9.1.6: {} + math-intrinsics@1.1.0: {} media-typer@1.1.0: {} @@ -6849,6 +7365,8 @@ snapshots: dependencies: lru.min: 1.1.4 + nanoid@3.3.15: {} + napi-postinstall@0.3.4: {} natural-compare@1.4.0: {} @@ -6928,6 +7446,8 @@ snapshots: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 + parse-srcset@1.0.2: {} + parseurl@1.3.3: {} path-exists@4.0.0: {} @@ -7011,6 +7531,12 @@ snapshots: possible-typed-array-names@1.1.0: {} + postcss@8.5.15: + dependencies: + nanoid: 3.3.15 + picocolors: 1.1.1 + source-map-js: 1.2.1 + postgres-array@2.0.0: {} postgres-array@3.0.4: {} @@ -7152,6 +7678,8 @@ snapshots: retry@0.12.0: {} + retry@0.13.1: {} + router@2.2.0: dependencies: debug: 4.4.3 @@ -7168,6 +7696,16 @@ snapshots: safer-buffer@2.1.2: {} + sanitize-html@2.17.5: + dependencies: + deepmerge: 4.3.1 + escape-string-regexp: 4.0.0 + htmlparser2: 10.1.0 + is-plain-object: 5.0.0 + launder: 1.7.1 + parse-srcset: 1.0.2 + postcss: 8.5.15 + scheduler@0.27.0: {} scrypt-js@3.0.1: {} @@ -7296,6 +7834,8 @@ snapshots: - supports-color - utf-8-validate + source-map-js@1.2.1: {} + source-map-support@0.5.13: dependencies: buffer-from: 1.1.2 @@ -7566,6 +8106,8 @@ snapshots: dependencies: makeerror: 1.0.12 + whatwg-mimetype@4.0.0: {} + which-module@2.0.1: {} which-typed-array@1.1.21: diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 1b7bfbd1..653a8815 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -245,3 +245,29 @@ model DecentralizedAsset { @@index([workspaceId, resourceType, resourceId]) @@map("decentralized_assets") } + +model WebhookSubscription { + id String @id @default(cuid()) + workspaceId String @default("default") + url String + secret String? + events Json // Array of event names e.g., ["lesson.completed"] + active Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([workspaceId, url]) + @@map("webhook_subscriptions") +} + +model StudentActivity { + id String @id @default(cuid()) + studentId String + courseId String + lessonId String + action String + timestamp DateTime @default(now()) + + @@map("student_activities") +} + diff --git a/backend/src/db/index.ts b/backend/src/db/index.ts index 98c372aa..654438c3 100644 --- a/backend/src/db/index.ts +++ b/backend/src/db/index.ts @@ -14,6 +14,7 @@ const workspaceModels = new Set([ 'LearningProgress', 'AuditLog', 'Canvas', + 'WebhookSubscription', ]); @@ -23,9 +24,15 @@ import { Pool } from 'pg'; import { PrismaPg } from '@prisma/adapter-pg'; const connectionString = `${process.env.DATABASE_URL}`; +const useSSL = + process.env.NODE_ENV !== 'test' && + !connectionString.includes('sslmode=disable') && + !connectionString.includes('localhost') && + !connectionString.includes('127.0.0.1'); + const pool = new Pool({ connectionString, - ssl: { rejectUnauthorized: false } + ssl: useSSL ? { rejectUnauthorized: false } : false }); const adapter = new PrismaPg(pool); diff --git a/backend/src/middleware/validation.ts b/backend/src/middleware/validation.ts index 1a540c4e..5f159085 100644 --- a/backend/src/middleware/validation.ts +++ b/backend/src/middleware/validation.ts @@ -26,7 +26,7 @@ export const subscriptionUpdateSchema = z.object({ export const validate = (schema: z.ZodSchema) => { return (req: Request, res: Response, next: NextFunction) => { try { - const validatedData = schema.parse(req.body); + const validatedData = schema.parse({ ...req.params, ...req.body }); req.body = validatedData; next(); } catch (error) { @@ -36,10 +36,16 @@ export const validate = (schema: z.ZodSchema) => { message: err.message, })); - return res.status(400).json(ApiResponse.error('Validation failed', errorMessages)); + return res.status(400).json({ + ...ApiResponse.error('Validation failed', errorMessages), + error: errorMessages.map((e) => `${e.field}: ${e.message}`).join(', '), + }); } - return res.status(500).json(ApiResponse.error('Internal server error')); + return res.status(500).json({ + ...ApiResponse.error('Internal server error'), + error: error instanceof Error ? error.message : 'Internal server error', + }); } }; }; diff --git a/backend/src/routes/certificates/validation.schemas.ts b/backend/src/routes/certificates/validation.schemas.ts index 2d2446eb..00704dcd 100644 --- a/backend/src/routes/certificates/validation.schemas.ts +++ b/backend/src/routes/certificates/validation.schemas.ts @@ -101,7 +101,7 @@ export type ReissueCertificateRequest = z.infer // Batch verification schema export const BatchVerificationSchema = z.object({ - tokenIds: z.array(z.string().min(1)).min(1).max(100), + tokenIds: z.array(z.string().min(1)).min(1, 'cannot be empty').max(100, 'Maximum 100'), }); export type BatchVerificationRequest = z.infer; diff --git a/backend/src/routes/index.ts b/backend/src/routes/index.ts index 265c70af..0594e6fa 100644 --- a/backend/src/routes/index.ts +++ b/backend/src/routes/index.ts @@ -19,6 +19,8 @@ import studentsRouter from './students.js'; import notificationRouter from '../notifications/notification.routes.js'; import metricsRouter from './metrics.routes.js'; +import webhooksRouter from './webhooks.js'; + const router = Router(); router.use('/health', healthRouter); @@ -36,7 +38,7 @@ router.use('/notifications', notificationRouter); router.use('/security', securityRouter); router.use('/generator', generatorRouter); router.use('/export', exportRouter); -// router.use('/webhooks', webhooksRouter); +router.use('/webhooks', webhooksRouter); router.use('/user', userRouter); router.use('/metrics', metricsRouter); diff --git a/backend/src/routes/learning/learning.routes.ts b/backend/src/routes/learning/learning.routes.ts index 18b723a0..ff4773f6 100644 --- a/backend/src/routes/learning/learning.routes.ts +++ b/backend/src/routes/learning/learning.routes.ts @@ -12,6 +12,10 @@ import { coursesQuerySchema, progressUpdateSchema, } from './validation.schemas.js'; +import prisma from '../../db/index.js'; +import cacheService from '../../cache/CacheService.js'; +import { MarkdownParserService } from '../../services/markdownParser.service.js'; +import { buildGatewayUrl } from '../../services/storage/utils.js'; const router = Router(); @@ -63,6 +67,55 @@ router.get( } ); +/** + * @route GET /api/learning/courses/:courseId/lessons/:lessonId/content + * @desc Get decentralized lesson content (Markdown/MDX parsed) + * @access Public + */ +router.get( + '/courses/:courseId/lessons/:lessonId/content', + async (req: Request, res: Response): Promise => { + try { + const { lessonId } = req.params; + + const cacheKey = `lesson:content:${lessonId}`; + const cached = await cacheService.get(cacheKey); + if (cached) { + res.json(cached); + return; + } + + const asset = await prisma.decentralizedAsset.findFirst({ + where: { + resourceType: 'lesson', + resourceId: lessonId, + }, + }); + + if (!asset) { + res.status(404).json({ error: 'Decentralized content not found for this lesson' }); + return; + } + + const gatewayUrl = buildGatewayUrl(asset.cid); + const response = await fetch(gatewayUrl); + if (!response.ok) { + throw new Error(`Failed to fetch content from gateway: ${response.statusText}`); + } + + const rawText = await response.text(); + const parsedContent = MarkdownParserService.parse(rawText); + + await cacheService.set(cacheKey, parsedContent, 3600 * 24); // Cache for 24 hours + + res.json(parsedContent); + } catch (error) { + console.error('Failed to retrieve decentralized lesson content:', error); + res.status(500).json({ error: 'Failed to retrieve lesson content' }); + } + } +); + /** * @route GET /api/learning/courses/:courseId/progress * @desc Get user progress for a specific course diff --git a/backend/src/routes/learning/learning.service.ts b/backend/src/routes/learning/learning.service.ts index 3da17282..c153467e 100644 --- a/backend/src/routes/learning/learning.service.ts +++ b/backend/src/routes/learning/learning.service.ts @@ -10,6 +10,7 @@ import { ProgressStatus, ProgressUpdateInput, } from './types.js'; +import { enqueueWebhookDeliveries } from '../../services/webhooks/index.js'; // In-memory mock store for demo resilience const mockProgressStore: Record = {}; @@ -118,6 +119,7 @@ export const listCourses = async (difficulty?: string): Promise ({ @@ -169,7 +171,7 @@ export const getCourseCurriculum = async ( return null; } - return { + const result = { id: course.id, title: course.title, description: course.description, @@ -179,6 +181,9 @@ export const getCourseCurriculum = async ( updatedAt: course.updatedAt, modules: filterModulesByDifficulty(getCurriculumForCourse(course.id), difficulty), }; + + await cacheService.set(cacheKey, result, cacheTTL.courses.curriculum); + return result; } catch (_error) { console.warn('Database error in getCourseCurriculum, falling back to mock data'); const mockCourse = COURSES.find((c) => c.id === courseId); @@ -231,7 +236,7 @@ export const getStudentProgress = async ( return p; } } catch (_error) { - console.warn('Database error in getStudentProgress, using mock store'); + console.warn('Database error in getStudentProgress, using mock store:', _error); } // Fallback to mock store @@ -289,7 +294,7 @@ export const updateStudentProgress = async ( if (input.status === 'completed') { if (!completedLessonSet.has(input.lessonId)) { // Log individual lesson completion activity - (prisma as any).studentActivity + await (prisma as any).studentActivity .create({ data: { studentId, @@ -299,6 +304,55 @@ export const updateStudentProgress = async ( }, }) .catch((err: any) => console.warn('Failed to log student activity:', err)); + + // Trigger lesson.completed webhook + try { + const student = await prisma.student.findUnique({ + where: { id: studentId }, + }); + + const subscriptions = await prisma.webhookSubscription.findMany({ + where: { active: true }, + }); + + const lessonCompletedSubscriptions = subscriptions.filter((sub) => { + try { + const events = typeof sub.events === 'string' ? JSON.parse(sub.events) : (sub.events as string[]); + return Array.isArray(events) && events.includes('lesson.completed'); + } catch { + return false; + } + }); + + if (lessonCompletedSubscriptions.length > 0) { + const payload = { + id: `evt_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`, + type: 'lesson.completed', + occurredAt: new Date().toISOString(), + source: 'student-portal', + data: { + studentId, + studentEmail: student?.email || '', + studentName: student ? `${student.firstName} ${student.lastName}` : '', + courseId, + lessonId: input.lessonId, + completedAt: new Date().toISOString(), + }, + }; + + const destinations = lessonCompletedSubscriptions.map((sub) => ({ + id: sub.id, + url: sub.url, + secret: sub.secret || undefined, + })); + + await enqueueWebhookDeliveries(payload as any, destinations).catch((err) => + console.error('Failed to enqueue lesson.completed webhooks:', err) + ); + } + } catch (err) { + console.error('Error during lesson completion webhook trigger:', err); + } } completedLessonSet.add(input.lessonId); } else { diff --git a/backend/src/routes/students.ts b/backend/src/routes/students.ts index 752c2670..33b56595 100644 --- a/backend/src/routes/students.ts +++ b/backend/src/routes/students.ts @@ -94,6 +94,7 @@ router.post('/', auditAction('CREATE_STUDENT', 'Student'), async (req, res) => { res.status(201).json(student); } catch (error) { + console.error("CREATE STUDENT ERROR:", error); if (error instanceof Error && error.message.startsWith('Invalid DID format')) { res.status(400).json({ error: error.message }); return; diff --git a/backend/src/routes/webhooks.ts b/backend/src/routes/webhooks.ts index e5e26f15..42051086 100644 --- a/backend/src/routes/webhooks.ts +++ b/backend/src/routes/webhooks.ts @@ -1,5 +1,6 @@ import { Request, Response, Router } from 'express'; import logger from '../utils/logger.js'; +import prisma from '../db/index.js'; import { canonicalizeWebhookPayload, enqueueWebhookDeliveries, @@ -76,4 +77,69 @@ router.post(['/ingest', '/dispatch'], async (req: Request, res: Response) => { } }); +// GET /subscriptions - Get all subscriptions +router.get('/subscriptions', async (req: Request, res: Response) => { + try { + const subscriptions = await prisma.webhookSubscription.findMany(); + return res.json(subscriptions); + } catch (error) { + logger.error('Failed to get webhook subscriptions:', error); + return res.status(500).json({ error: 'Failed to fetch webhook subscriptions' }); + } +}); + +// POST /subscriptions - Create a new subscription +router.post('/subscriptions', async (req: Request, res: Response) => { + try { + const { url, secret, events, active } = req.body; + + if (!url || typeof url !== 'string' || (!url.startsWith('http://') && !url.startsWith('https://'))) { + return res.status(400).json({ error: 'Invalid subscription URL' }); + } + + if (!events || !Array.isArray(events) || events.length === 0 || !events.every(e => typeof e === 'string')) { + return res.status(400).json({ error: 'Events must be a non-empty array of strings' }); + } + + const subscription = await prisma.webhookSubscription.create({ + data: { + url, + secret: secret || null, + events: events as any, + active: active !== undefined ? Boolean(active) : true, + }, + }); + + return res.status(201).json(subscription); + } catch (error: any) { + logger.error('Failed to create webhook subscription:', error); + if (error.code === 'P2002') { + return res.status(409).json({ error: 'Subscription for this URL already exists' }); + } + return res.status(500).json({ error: 'Failed to create webhook subscription' }); + } +}); + +// DELETE /subscriptions/:id - Delete a subscription +router.delete('/subscriptions/:id', async (req: Request, res: Response) => { + try { + const { id } = req.params; + + const exists = await prisma.webhookSubscription.findUnique({ + where: { id }, + }); + if (!exists) { + return res.status(404).json({ error: 'Subscription not found' }); + } + + await prisma.webhookSubscription.delete({ + where: { id }, + }); + return res.status(204).send(); + } catch (error) { + logger.error('Failed to delete webhook subscription:', error); + return res.status(500).json({ error: 'Failed to delete webhook subscription' }); + } +}); + export default router; diff --git a/backend/src/services/markdownParser.service.ts b/backend/src/services/markdownParser.service.ts new file mode 100644 index 00000000..a66acf7a --- /dev/null +++ b/backend/src/services/markdownParser.service.ts @@ -0,0 +1,62 @@ +import { marked } from 'marked'; +import sanitizeHtml from 'sanitize-html'; + +export interface ParsedCodeBlock { + language: string; + code: string; +} + +export interface ParsedContent { + html: string; + codeBlocks: ParsedCodeBlock[]; +} + +export class MarkdownParserService { + /** + * Parse Markdown or MDX content into sanitized HTML and tokenized code blocks. + * Ensures safety against XSS attacks. + */ + public static parse(content: string): ParsedContent { + if (!content) { + return { html: '', codeBlocks: [] }; + } + + // Compile Markdown to HTML + const rawHtml = marked.parse(content) as string; + + // Sanitize HTML to prevent XSS (allowing standard formatting tags, stripping scripts/event handlers) + const html = sanitizeHtml(rawHtml, { + allowedTags: sanitizeHtml.defaults.allowedTags.concat([ + 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'img', 'span', 'div' + ]), + allowedAttributes: { + ...sanitizeHtml.defaults.allowedAttributes, + img: ['src', 'alt', 'title', 'width', 'height'], + }, + }); + + // Extract code blocks using marked lexer + const tokens = marked.lexer(content); + const codeBlocks: ParsedCodeBlock[] = []; + + const traverse = (tokenList: any[]) => { + for (const token of tokenList) { + if (token.type === 'code') { + codeBlocks.push({ + language: token.lang || 'text', + code: token.text, + }); + } else if (token.tokens) { + traverse(token.tokens); + } + } + }; + + traverse(tokens); + + return { + html, + codeBlocks, + }; + } +} diff --git a/backend/src/utils/redis.ts b/backend/src/utils/redis.ts index 745d695f..98e63144 100644 --- a/backend/src/utils/redis.ts +++ b/backend/src/utils/redis.ts @@ -31,6 +31,8 @@ const createTestRedisClient = () => { }, lpush: async (_key: string, ...values: string[]) => values.length, brpop: async () => null, + publish: async (_channel: string, _message: string) => 0, + subscribe: (..._args: any[]) => undefined, }; }; diff --git a/backend/tests/curriculum-progress.integration.test.ts b/backend/tests/curriculum-progress.integration.test.ts index 39562391..2d1016d0 100644 --- a/backend/tests/curriculum-progress.integration.test.ts +++ b/backend/tests/curriculum-progress.integration.test.ts @@ -59,7 +59,7 @@ beforeAll(async () => { dbAvailable = true; console.log('✓ Database connection established for integration tests'); } catch (error) { - console.warn('⚠ Database not available, skipping integration tests'); + console.warn('⚠ Database not available, skipping integration tests', error); } }); @@ -69,9 +69,7 @@ afterAll(async () => { } }); -const describeOrSkip = dbAvailable ? describe : describe.skip; - -describeOrSkip('Curriculum & Progress Integration Tests', () => { +describe('Curriculum & Progress Integration Tests', () => { // Clean database before each test beforeEach(async () => { if (!dbAvailable) return; diff --git a/backend/tests/curriculum.unit.test.ts b/backend/tests/curriculum.unit.test.ts index 4670f220..5e5f83fe 100644 --- a/backend/tests/curriculum.unit.test.ts +++ b/backend/tests/curriculum.unit.test.ts @@ -6,7 +6,7 @@ */ import { jest } from '@jest/globals'; -import type { Course } from '@prisma/client'; +type Course = any; // Mock dependencies before imports const mockPrismaFindMany = jest.fn(); @@ -14,7 +14,7 @@ const mockPrismaFindUnique = jest.fn(); const mockCacheGet = jest.fn(); const mockCacheSet = jest.fn(); -jest.unstable_mockModule('../src/db/index.js', () => ({ +jest.unstable_mockModule('../src/db/index.ts', () => ({ default: { course: { findMany: mockPrismaFindMany, diff --git a/backend/tests/jest.setup.ts b/backend/tests/jest.setup.ts index fb435536..55c6ba07 100644 --- a/backend/tests/jest.setup.ts +++ b/backend/tests/jest.setup.ts @@ -1,3 +1,8 @@ +import dotenv from 'dotenv'; +import path from 'path'; + +dotenv.config({ path: path.resolve(process.cwd(), '.env.test') }); + process.env.NODE_ENV = 'test'; process.env.REDIS_URL = process.env.REDIS_URL || 'redis://localhost:6379'; diff --git a/backend/tests/learning.content.test.ts b/backend/tests/learning.content.test.ts new file mode 100644 index 00000000..f7ebc3db --- /dev/null +++ b/backend/tests/learning.content.test.ts @@ -0,0 +1,82 @@ +import request from 'supertest'; +import { describe, expect, it, beforeAll, afterAll, jest } from '@jest/globals'; +import { app } from '../src/index.js'; +import prisma from '../src/db/index.js'; + +describe('Decentralized Storage Lesson Content API Tests', () => { + const workspaceId = 'default'; + const lessonId = 'lesson-ipfs-test'; + const cid = 'bafy-mock-cid-lesson-content'; + let assetId: string; + const originalFetch = global.fetch; + + beforeAll(async () => { + // Mock global fetch to return a mock markdown content + global.fetch = jest.fn().mockImplementation(() => { + return Promise.resolve({ + ok: true, + text: () => Promise.resolve('# Test Lesson\n\nWelcome to this **decentralized** lesson.\n\n```javascript\nconst a = 42;\n```'), + } as Response); + }) as any; + + // Create a mock DecentralizedAsset in the DB + const asset = await prisma.decentralizedAsset.create({ + data: { + workspaceId, + resourceType: 'lesson', + resourceId: lessonId, + name: 'content.md', + kind: 'generic', + provider: 'pinata', + cid, + ipfsUri: `ipfs://${cid}`, + gatewayUrl: `https://gateway.pinata.cloud/ipfs/${cid}`, + status: 'pinned', + }, + }); + assetId = asset.id; + }); + + afterAll(async () => { + // Restore global fetch + global.fetch = originalFetch; + + // Cleanup DB + if (assetId) { + await prisma.decentralizedAsset.deleteMany({ + where: { id: assetId }, + }).catch(() => {}); + } + await prisma.$disconnect(); + }); + + it('should retrieve and parse decentralized lesson content successfully', async () => { + const response = await request(app) + .get(`/api/v1/learning/courses/course-1/lessons/${lessonId}/content`) + .set('x-workspace-id', workspaceId); + + expect(response.status).toBe(200); + expect(response.body).toHaveProperty('html'); + expect(response.body).toHaveProperty('codeBlocks'); + + // HTML should be parsed and sanitized + expect(response.body.html).toContain('

Test Lesson

'); + expect(response.body.html).toContain('decentralized'); + + // Code blocks should be extracted + expect(response.body.codeBlocks).toHaveLength(1); + expect(response.body.codeBlocks[0]).toEqual({ + language: 'javascript', + code: 'const a = 42;', + }); + }); + + it('should return 404 if decentralized content asset does not exist', async () => { + const response = await request(app) + .get('/api/v1/learning/courses/course-1/lessons/non-existent-lesson/content') + .set('x-workspace-id', workspaceId); + + expect(response.status).toBe(404); + expect(response.body.error).toContain('not found'); + }); +}); diff --git a/backend/tests/markdownParser.service.test.ts b/backend/tests/markdownParser.service.test.ts new file mode 100644 index 00000000..bf29794a --- /dev/null +++ b/backend/tests/markdownParser.service.test.ts @@ -0,0 +1,54 @@ +import { describe, expect, it } from '@jest/globals'; +import { MarkdownParserService } from '../src/services/markdownParser.service.js'; + +describe('MarkdownParserService', () => { + it('should compile simple markdown to HTML', () => { + const markdown = '# Hello World\nThis is **bold** text.'; + const result = MarkdownParserService.parse(markdown); + + expect(result.html).toContain('

Hello World

'); + expect(result.html).toContain('This is bold text.'); + }); + + it('should sanitize HTML elements to prevent XSS payloads', () => { + const xssMarkdown = 'Some text and '; + const result = MarkdownParserService.parse(xssMarkdown); + + expect(result.html).not.toContain('