diff --git a/.env b/.env index 5a09d7d..8a016d3 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ -mongo_uri=`mongodb+srv://nadhunadhil33429_db_user:Admin123shanu@cluster1.noy1nfg.mongodb.net/Instaviz?retryWrites=true&w=majority&appName=Cluster1` +mongo_uri=mongodb+srv://nadhunadhil33429_db_user:Admin123shanu@cluster1.noy1nfg.mongodb.net/Instaviz?retryWrites=true&w=majority&appName=Cluster1 STRIPE_SECRET_KEY=sk_test_51SQ1dLIVdMwHNhBBbCZWVVfYjIuqQocBHae6gnVS2kDwBY3kKEByP6xWhES6AAKA30VZi5SXZxNpfKMCDLOaMa8a00yKxvydpG CLIENT_URL=http://localhost:3000 PORT=5000 @@ -14,3 +14,19 @@ REFRESH_SECRET = E72b1f2a9e7c8c1f4e28e5c948b0f21aebda10a4a18e9d3e7e1a8c3e5e4b5a EMAIL = shanu.work.org@gmail.com EMAIL_PASS = lhubgdnbvmfrzjcj + + +# supabase key and secret + +SUPABASE_URL=https://ymhpjbqatqaqarydzijp.supabase.co +SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InltaHBqYnFhdHFhcWFyeWR6aWpwIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjI4NDkzNTksImV4cCI6MjA3ODQyNTM1OX0.O5HReAQB7nK5YZN9si3-ybwvPCOPwUAg-sM2Uw-pKjo +MAX_FILE_SIZE = 50 * 1024 * 1024; +GEMINI_API_KEY =AIzaSyBkUAGwR5nE36CFk1cGPw6bOlSl4OQK1Ro,AIzaSyD_UUtu-zR4CUwOB3Z7xyLMmcl1ooiPxJ8,AIzaSyBnhzklFEmwqi1YkmrNqVSKrZTO9ljUJo4,AIzaSyBza-e377zkiwVi8ZgS6Jgx9sGfMhp0i6o,AIzaSyDD1UTnDWTQGcX-Mtxj9xuVHNkXL3dG-sY,AIzaSyDxdv7foLvaFLRG3qTVGqENMwuXoZzuKlo,AIzaSyD8Vp6w5NMOqlNrMLUDdTXmAwx_28jYaVo,AIzaSyBRJBYaiLfr5YN3hqCBbD_aGUnOgkBZIOU +# GEMINI_API_LIGHT_KEY =AIzaSyC_YEplnqKTokFHMkL0QxQDd98M05JGzG8 +GEMINI_API_LIGHT_KEY =AIzaSyBbSe9bzD7wbK5utG5TkWvIemYt_QMoQiQ + +R2_ACCOUNT_ID=e6a357e38f300a67ae9df3c103d060e5 +R2_ACCESS_KEY_ID=c7d19730f99bbbebe4158c94089efd4a +R2_SECRET_ACCESS_KEY=5e79e90773faa628cd5409ebd9835ffd140bca3e8b59002085e3a634e94613da +R2_BUCKET_NAME=instaviz +R2_PUBLIC_URL=https://pub-e11ccec3e5ab495796b8d807084b93ca.r2.dev diff --git a/package-lock.json b/package-lock.json index 1dabc77..888edb8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,41 +9,178 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@aws-sdk/client-s3": "^3.930.0", + "@google/generative-ai": "^0.24.1", + "@modelcontextprotocol/sdk": "^1.22.0", + "@supabase/supabase-js": "^2.81.0", "@types/bcrypt": "^6.0.0", "@types/jsonwebtoken": "^9.0.10", + "@types/morgan": "^1.9.10", "@types/passport": "^1.0.17", "@types/passport-google-oauth20": "^2.0.17", "bcrypt": "^6.0.0", "body-parser": "^2.2.0", "cookie-parser": "^1.4.7", "cors": "^2.8.5", + "csv-parser": "^3.2.0", "dotenv": "^17.2.3", "express": "^5.1.0", + "express-rate-limit": "^8.2.1", "joi": "^18.0.1", "jsonwebtoken": "^9.0.2", "mongoose": "^8.19.1", + "morgan": "^1.10.1", + "multer": "^2.0.2", + "node-fetch": "^3.3.2", "nodemailer": "^7.0.10", "passport": "^0.7.0", "passport-google-oauth20": "^2.0.0", - "stripe": "^19.2.1" + "stripe": "^19.2.1", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1", + "yamljs": "^0.3.0" }, "devDependencies": { "@types/cookie-parser": "^1.4.10", "@types/cors": "^2.8.19", "@types/express": "^5.0.5", "@types/mongoose": "^5.11.96", + "@types/multer": "^2.0.0", "@types/node": "^24.10.0", "@types/nodemailer": "^7.0.3", + "@types/swagger-jsdoc": "^6.0.4", + "@types/swagger-ui-express": "^4.1.8", "nodemon": "^3.1.10", + "prettier": "^3.6.2", "ts-node": "^10.9.2", "typescript": "^5.9.3" } }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz", + "integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==", + "license": "MIT", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.6", + "call-me-maybe": "^1.0.1", + "js-yaml": "^4.1.0" + } + }, + "node_modules/@apidevtools/openapi-schemas": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz", + "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@apidevtools/swagger-methods": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", + "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==", + "license": "MIT" + }, + "node_modules/@apidevtools/swagger-parser": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz", + "integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==", + "license": "MIT", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^9.0.6", + "@apidevtools/openapi-schemas": "^2.0.4", + "@apidevtools/swagger-methods": "^3.0.2", + "@jsdevtools/ono": "^7.1.3", + "call-me-maybe": "^1.0.1", + "z-schema": "^5.0.1" + }, + "peerDependencies": { + "openapi-types": ">=7" + } + }, + "node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/crc32c": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz", + "integrity": "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-5.2.0.tgz", + "integrity": "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@aws-crypto/sha256-browser": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", @@ -59,7 +196,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", - "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -72,7 +208,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/is-array-buffer": "^2.2.0", @@ -86,7 +221,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/util-buffer-from": "^2.2.0", @@ -100,7 +234,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-crypto/util": "^5.2.0", @@ -115,7 +248,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", - "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -125,7 +257,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.222.0", @@ -137,7 +268,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", - "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -150,7 +280,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/is-array-buffer": "^2.2.0", @@ -164,7 +293,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/util-buffer-from": "^2.2.0", @@ -174,6 +302,552 @@ "node": ">=14.0.0" } }, + "node_modules/@aws-sdk/client-s3": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.930.0.tgz", + "integrity": "sha512-5ddhr3ShseFRIdNXH8bkh1CIC78p0ZXpa7HJZpONDU3JGvd/B+yHHgTr141rtgoFTaW0gjPdbdtqtPLnhlnK+A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha1-browser": "5.2.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.930.0", + "@aws-sdk/credential-provider-node": "3.930.0", + "@aws-sdk/middleware-bucket-endpoint": "3.930.0", + "@aws-sdk/middleware-expect-continue": "3.930.0", + "@aws-sdk/middleware-flexible-checksums": "3.930.0", + "@aws-sdk/middleware-host-header": "3.930.0", + "@aws-sdk/middleware-location-constraint": "3.930.0", + "@aws-sdk/middleware-logger": "3.930.0", + "@aws-sdk/middleware-recursion-detection": "3.930.0", + "@aws-sdk/middleware-sdk-s3": "3.930.0", + "@aws-sdk/middleware-ssec": "3.930.0", + "@aws-sdk/middleware-user-agent": "3.930.0", + "@aws-sdk/region-config-resolver": "3.930.0", + "@aws-sdk/signature-v4-multi-region": "3.930.0", + "@aws-sdk/types": "3.930.0", + "@aws-sdk/util-endpoints": "3.930.0", + "@aws-sdk/util-user-agent-browser": "3.930.0", + "@aws-sdk/util-user-agent-node": "3.930.0", + "@aws-sdk/xml-builder": "3.930.0", + "@smithy/config-resolver": "^4.4.3", + "@smithy/core": "^3.18.2", + "@smithy/eventstream-serde-browser": "^4.2.5", + "@smithy/eventstream-serde-config-resolver": "^4.3.5", + "@smithy/eventstream-serde-node": "^4.2.5", + "@smithy/fetch-http-handler": "^5.3.6", + "@smithy/hash-blob-browser": "^4.2.6", + "@smithy/hash-node": "^4.2.5", + "@smithy/hash-stream-node": "^4.2.5", + "@smithy/invalid-dependency": "^4.2.5", + "@smithy/md5-js": "^4.2.5", + "@smithy/middleware-content-length": "^4.2.5", + "@smithy/middleware-endpoint": "^4.3.9", + "@smithy/middleware-retry": "^4.4.9", + "@smithy/middleware-serde": "^4.2.5", + "@smithy/middleware-stack": "^4.2.5", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/node-http-handler": "^4.4.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/smithy-client": "^4.9.5", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.8", + "@smithy/util-defaults-mode-node": "^4.2.11", + "@smithy/util-endpoints": "^3.2.5", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-retry": "^4.2.5", + "@smithy/util-stream": "^4.5.6", + "@smithy/util-utf8": "^4.2.0", + "@smithy/util-waiter": "^4.2.5", + "@smithy/uuid": "^1.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/client-sso": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.930.0.tgz", + "integrity": "sha512-sASqgm1iMLcmi+srSH9WJuqaf3GQAKhuB4xIJwkNEPUQ+yGV8HqErOOHJLXXuTUyskcdtK+4uMaBRLT2ESm+QQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.930.0", + "@aws-sdk/middleware-host-header": "3.930.0", + "@aws-sdk/middleware-logger": "3.930.0", + "@aws-sdk/middleware-recursion-detection": "3.930.0", + "@aws-sdk/middleware-user-agent": "3.930.0", + "@aws-sdk/region-config-resolver": "3.930.0", + "@aws-sdk/types": "3.930.0", + "@aws-sdk/util-endpoints": "3.930.0", + "@aws-sdk/util-user-agent-browser": "3.930.0", + "@aws-sdk/util-user-agent-node": "3.930.0", + "@smithy/config-resolver": "^4.4.3", + "@smithy/core": "^3.18.2", + "@smithy/fetch-http-handler": "^5.3.6", + "@smithy/hash-node": "^4.2.5", + "@smithy/invalid-dependency": "^4.2.5", + "@smithy/middleware-content-length": "^4.2.5", + "@smithy/middleware-endpoint": "^4.3.9", + "@smithy/middleware-retry": "^4.4.9", + "@smithy/middleware-serde": "^4.2.5", + "@smithy/middleware-stack": "^4.2.5", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/node-http-handler": "^4.4.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/smithy-client": "^4.9.5", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.8", + "@smithy/util-defaults-mode-node": "^4.2.11", + "@smithy/util-endpoints": "^3.2.5", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-retry": "^4.2.5", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/core": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.930.0.tgz", + "integrity": "sha512-E95pWT1ayfRWg0AW2KNOCYM7QQcVeOhMRLX5PXLeDKcdxP7s3x0LHG9t7a3nPbAbvYLRrhC7O2lLWzzMCpqjsw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.930.0", + "@aws-sdk/xml-builder": "3.930.0", + "@smithy/core": "^3.18.2", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/signature-v4": "^5.3.5", + "@smithy/smithy-client": "^4.9.5", + "@smithy/types": "^4.9.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.930.0.tgz", + "integrity": "sha512-5tJyxNQmm9C1XKeiWt/K67mUHtTiU2FxTkVsqVrzAMjNsF3uyA02kyTK70byh5n29oVR9XNValVEl6jk01ipYg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.930.0", + "@aws-sdk/types": "3.930.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.930.0.tgz", + "integrity": "sha512-vw565GctpOPoRJyRvgqXM8U/4RG8wYEPfhe6GHvt9dchebw0OaFeW1mmSYpwEPkMhZs9Z808dkSPScwm8WZBKA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.930.0", + "@aws-sdk/types": "3.930.0", + "@smithy/fetch-http-handler": "^5.3.6", + "@smithy/node-http-handler": "^4.4.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/smithy-client": "^4.9.5", + "@smithy/types": "^4.9.0", + "@smithy/util-stream": "^4.5.6", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.930.0.tgz", + "integrity": "sha512-Ua4T5MWjm7QdHi7ZSUvnPBFwBZmLFP/IEGCLacPKbUT1sQO30hlWuB/uQOj0ns4T6p7V4XsM8bz5+xsW2yRYbQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.930.0", + "@aws-sdk/credential-provider-env": "3.930.0", + "@aws-sdk/credential-provider-http": "3.930.0", + "@aws-sdk/credential-provider-process": "3.930.0", + "@aws-sdk/credential-provider-sso": "3.930.0", + "@aws-sdk/credential-provider-web-identity": "3.930.0", + "@aws-sdk/nested-clients": "3.930.0", + "@aws-sdk/types": "3.930.0", + "@smithy/credential-provider-imds": "^4.2.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.930.0.tgz", + "integrity": "sha512-LTx5G0PsL51hNCCzOIdacGPwqnTp3X2Ck8CjLL4Kz9FTR0mfY02qEJB5y5segU1hlge/WdQYxzBBMhtMUR2h8A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.930.0", + "@aws-sdk/credential-provider-http": "3.930.0", + "@aws-sdk/credential-provider-ini": "3.930.0", + "@aws-sdk/credential-provider-process": "3.930.0", + "@aws-sdk/credential-provider-sso": "3.930.0", + "@aws-sdk/credential-provider-web-identity": "3.930.0", + "@aws-sdk/types": "3.930.0", + "@smithy/credential-provider-imds": "^4.2.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.930.0.tgz", + "integrity": "sha512-lqC4lepxgwR2uZp/JROTRjkHld4/FEpSgofmiIOAfUfDx0OWSg7nkWMMS/DzlMpODqATl9tO0DcvmIJ8tMbh6g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.930.0", + "@aws-sdk/types": "3.930.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.930.0.tgz", + "integrity": "sha512-LIs2aaVoFfioRokR1R9SpLS9u8CmbHhrV/gpHO1ED41qNCujn23vAxRNQmWzJ2XoCxSTwvToiHD2i6CjPA6rHQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.930.0", + "@aws-sdk/core": "3.930.0", + "@aws-sdk/token-providers": "3.930.0", + "@aws-sdk/types": "3.930.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.930.0.tgz", + "integrity": "sha512-iIYF8GReLOp16yn2bnRWrc4UOW/vVLifqyRWZ3iAGe8NFzUiHBq+Nok7Edh+2D8zt30QOCOsWCZ31uRrPuXH8w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.930.0", + "@aws-sdk/nested-clients": "3.930.0", + "@aws-sdk/types": "3.930.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.930.0.tgz", + "integrity": "sha512-x30jmm3TLu7b/b+67nMyoV0NlbnCVT5DI57yDrhXAPCtdgM1KtdLWt45UcHpKOm1JsaIkmYRh2WYu7Anx4MG0g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.930.0", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-logger": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.930.0.tgz", + "integrity": "sha512-vh4JBWzMCBW8wREvAwoSqB2geKsZwSHTa0nSt0OMOLp2PdTYIZDi0ZiVMmpfnjcx9XbS6aSluLv9sKx4RrG46A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.930.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.930.0.tgz", + "integrity": "sha512-gv0sekNpa2MBsIhm2cjP3nmYSfI4nscx/+K9u9ybrWZBWUIC4kL2sV++bFjjUz4QxUIlvKByow3/a9ARQyCu7Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.930.0", + "@aws/lambda-invoke-store": "^0.1.1", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.930.0.tgz", + "integrity": "sha512-bnVK0xVVmrPyKTbV5MgG6KP7MPe87GngBPD5MrYj9kWmGrJIvnt0qer0UIgWAnsyCi7XrTfw7SMgYRpSxOYEMw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.930.0", + "@aws-sdk/types": "3.930.0", + "@aws-sdk/util-arn-parser": "3.893.0", + "@smithy/core": "^3.18.2", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/signature-v4": "^5.3.5", + "@smithy/smithy-client": "^4.9.5", + "@smithy/types": "^4.9.0", + "@smithy/util-config-provider": "^4.2.0", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-stream": "^4.5.6", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.930.0.tgz", + "integrity": "sha512-UUItqy02biaHoZDd1Z2CskFon3Lej15ZCIZzW4n2lsJmgLWNvz21jtFA8DQny7ZgCLAOOXI8YK3VLZptZWtIcg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.930.0", + "@aws-sdk/types": "3.930.0", + "@aws-sdk/util-endpoints": "3.930.0", + "@smithy/core": "^3.18.2", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/nested-clients": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.930.0.tgz", + "integrity": "sha512-eEDjTVXNiDkoV0ZV+X+WV40GTpF70xZmDW13CQzQF7rzOC2iFjtTRU+F7MUhy/Vs+e9KvDgiuCDecITtaOXUNw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.930.0", + "@aws-sdk/middleware-host-header": "3.930.0", + "@aws-sdk/middleware-logger": "3.930.0", + "@aws-sdk/middleware-recursion-detection": "3.930.0", + "@aws-sdk/middleware-user-agent": "3.930.0", + "@aws-sdk/region-config-resolver": "3.930.0", + "@aws-sdk/types": "3.930.0", + "@aws-sdk/util-endpoints": "3.930.0", + "@aws-sdk/util-user-agent-browser": "3.930.0", + "@aws-sdk/util-user-agent-node": "3.930.0", + "@smithy/config-resolver": "^4.4.3", + "@smithy/core": "^3.18.2", + "@smithy/fetch-http-handler": "^5.3.6", + "@smithy/hash-node": "^4.2.5", + "@smithy/invalid-dependency": "^4.2.5", + "@smithy/middleware-content-length": "^4.2.5", + "@smithy/middleware-endpoint": "^4.3.9", + "@smithy/middleware-retry": "^4.4.9", + "@smithy/middleware-serde": "^4.2.5", + "@smithy/middleware-stack": "^4.2.5", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/node-http-handler": "^4.4.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/smithy-client": "^4.9.5", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.8", + "@smithy/util-defaults-mode-node": "^4.2.11", + "@smithy/util-endpoints": "^3.2.5", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-retry": "^4.2.5", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.930.0.tgz", + "integrity": "sha512-KL2JZqH6aYeQssu1g1KuWsReupdfOoxD6f1as2VC+rdwYFUu4LfzMsFfXnBvvQWWqQ7rZHWOw1T+o5gJmg7Dzw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.930.0", + "@smithy/config-resolver": "^4.4.3", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.930.0.tgz", + "integrity": "sha512-UOAq1ftbrZc9HRP/nG970OONNykIDWunjth9GvGDODkW0FR7DHJWBmTwj61ZnrSiuSParp1eQfa+JsZ8eXNPcw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-sdk-s3": "3.930.0", + "@aws-sdk/types": "3.930.0", + "@smithy/protocol-http": "^5.3.5", + "@smithy/signature-v4": "^5.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/token-providers": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.930.0.tgz", + "integrity": "sha512-K+fJFJXA2Tdx10WhhTm+xQmf1WDHu14rUutByyqx6W0iW2rhtl3YeRr188LWSU3/hpz7BPyvigaAb0QyRti6FQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.930.0", + "@aws-sdk/nested-clients": "3.930.0", + "@aws-sdk/types": "3.930.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/types": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.930.0.tgz", + "integrity": "sha512-we/vaAgwlEFW7IeftmCLlLMw+6hFs3DzZPJw7lVHbj/5HJ0bz9gndxEsS2lQoeJ1zhiiLqAqvXxmM43s0MBg0A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/util-endpoints": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.930.0.tgz", + "integrity": "sha512-M2oEKBzzNAYr136RRc6uqw3aWlwCxqTP1Lawps9E1d2abRPvl1p1ztQmmXp1Ak4rv8eByIZ+yQyKQ3zPdRG5dw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.930.0", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "@smithy/util-endpoints": "^3.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.930.0.tgz", + "integrity": "sha512-q6lCRm6UAe+e1LguM5E4EqM9brQlDem4XDcQ87NzEvlTW6GzmNCO0w1jS0XgCFXQHjDxjdlNFX+5sRbHijwklg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.930.0", + "@smithy/types": "^4.9.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.930.0.tgz", + "integrity": "sha512-tYc5uFKogn0vLukeZ6Zz2dR1/WiTjxZH7+Jjoce6aEYgRVfyrDje1POFb7YxhNZ7Pp1WzHCuwW2KgkmMoYVbxQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.930.0", + "@aws-sdk/types": "3.930.0", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/xml-builder": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.930.0.tgz", + "integrity": "sha512-YIfkD17GocxdmlUVc3ia52QhcWuRIUJonbF8A2CYfcWNV3HzvAqpcPeC0bYUhkK+8e8YO1ARnLKZQE0TlwzorA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "fast-xml-parser": "5.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@aws-sdk/client-sesv2": { "version": "3.922.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-sesv2/-/client-sesv2-3.922.0.tgz", @@ -446,6 +1120,140 @@ "node": ">=18.0.0" } }, + "node_modules/@aws-sdk/middleware-bucket-endpoint": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.930.0.tgz", + "integrity": "sha512-cnCLWeKPYgvV4yRYPFH6pWMdUByvu2cy2BAlfsPpvnm4RaVioztyvxmQj5PmVN5fvWs5w/2d6U7le8X9iye2sA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.930.0", + "@aws-sdk/util-arn-parser": "3.893.0", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "@smithy/util-config-provider": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-bucket-endpoint/node_modules/@aws-sdk/types": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.930.0.tgz", + "integrity": "sha512-we/vaAgwlEFW7IeftmCLlLMw+6hFs3DzZPJw7lVHbj/5HJ0bz9gndxEsS2lQoeJ1zhiiLqAqvXxmM43s0MBg0A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-expect-continue": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.930.0.tgz", + "integrity": "sha512-5HEQ+JU4DrLNWeY27wKg/jeVa8Suy62ivJHOSUf6e6hZdVIMx0h/kXS1fHEQNNiLu2IzSEP/bFXsKBaW7x7s0g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.930.0", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-expect-continue/node_modules/@aws-sdk/types": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.930.0.tgz", + "integrity": "sha512-we/vaAgwlEFW7IeftmCLlLMw+6hFs3DzZPJw7lVHbj/5HJ0bz9gndxEsS2lQoeJ1zhiiLqAqvXxmM43s0MBg0A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-flexible-checksums": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.930.0.tgz", + "integrity": "sha512-ZbAwwe7sqIO7tmNH3Q8rH91tZCQwBGliyXUbANoVMp1CWLPDAeaECurkmuBe/UJmoszi2U3gyhIQlbfzD2SBLw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@aws-crypto/crc32c": "5.2.0", + "@aws-crypto/util": "5.2.0", + "@aws-sdk/core": "3.930.0", + "@aws-sdk/types": "3.930.0", + "@smithy/is-array-buffer": "^4.2.0", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-stream": "^4.5.6", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-flexible-checksums/node_modules/@aws-sdk/core": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.930.0.tgz", + "integrity": "sha512-E95pWT1ayfRWg0AW2KNOCYM7QQcVeOhMRLX5PXLeDKcdxP7s3x0LHG9t7a3nPbAbvYLRrhC7O2lLWzzMCpqjsw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.930.0", + "@aws-sdk/xml-builder": "3.930.0", + "@smithy/core": "^3.18.2", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/signature-v4": "^5.3.5", + "@smithy/smithy-client": "^4.9.5", + "@smithy/types": "^4.9.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-flexible-checksums/node_modules/@aws-sdk/types": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.930.0.tgz", + "integrity": "sha512-we/vaAgwlEFW7IeftmCLlLMw+6hFs3DzZPJw7lVHbj/5HJ0bz9gndxEsS2lQoeJ1zhiiLqAqvXxmM43s0MBg0A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-flexible-checksums/node_modules/@aws-sdk/xml-builder": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.930.0.tgz", + "integrity": "sha512-YIfkD17GocxdmlUVc3ia52QhcWuRIUJonbF8A2CYfcWNV3HzvAqpcPeC0bYUhkK+8e8YO1ARnLKZQE0TlwzorA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "fast-xml-parser": "5.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@aws-sdk/middleware-host-header": { "version": "3.922.0", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.922.0.tgz", @@ -462,6 +1270,33 @@ "node": ">=18.0.0" } }, + "node_modules/@aws-sdk/middleware-location-constraint": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.930.0.tgz", + "integrity": "sha512-QIGNsNUdRICog+LYqmtJ03PLze6h2KCORXUs5td/hAEjVP5DMmubhtrGg1KhWyctACluUH/E/yrD14p4pRXxwA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.930.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-location-constraint/node_modules/@aws-sdk/types": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.930.0.tgz", + "integrity": "sha512-we/vaAgwlEFW7IeftmCLlLMw+6hFs3DzZPJw7lVHbj/5HJ0bz9gndxEsS2lQoeJ1zhiiLqAqvXxmM43s0MBg0A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@aws-sdk/middleware-logger": { "version": "3.922.0", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.922.0.tgz", @@ -520,6 +1355,33 @@ "node": ">=18.0.0" } }, + "node_modules/@aws-sdk/middleware-ssec": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.930.0.tgz", + "integrity": "sha512-N2/SvodmaDS6h7CWfuapt3oJyn1T2CBz0CsDIiTDv9cSagXAVFjPdm2g4PFJqrNBeqdDIoYBnnta336HmamWHg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.930.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-ssec/node_modules/@aws-sdk/types": { + "version": "3.930.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.930.0.tgz", + "integrity": "sha512-we/vaAgwlEFW7IeftmCLlLMw+6hFs3DzZPJw7lVHbj/5HJ0bz9gndxEsS2lQoeJ1zhiiLqAqvXxmM43s0MBg0A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@aws-sdk/middleware-user-agent": { "version": "3.922.0", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.922.0.tgz", @@ -647,7 +1509,6 @@ "version": "3.922.0", "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.922.0.tgz", "integrity": "sha512-eLA6XjVobAUAMivvM7DBL79mnHyrm+32TkXNWZua5mnxF+6kQCfblKKJvxMZLGosO53/Ex46ogim8IY5Nbqv2w==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.8.1", @@ -661,7 +1522,6 @@ "version": "3.893.0", "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.893.0.tgz", "integrity": "sha512-u8H4f2Zsi19DGnwj5FSZzDMhytYF/bCh37vAtBsn3cNDL3YG578X5oc+wSX54pM3tOxS+NY7tvOAo52SW7koUA==", - "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -691,7 +1551,6 @@ "version": "3.893.0", "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.893.0.tgz", "integrity": "sha512-T89pFfgat6c8nMmpI8eKjBcDcgJq36+m9oiXbcUzeU55MP9ZuGgBomGjGnHaEyF36jenW9gmg3NfZDm0AO2XPg==", - "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -757,7 +1616,6 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.1.1.tgz", "integrity": "sha512-RcLam17LdlbSOSp9VxmUu1eI6Mwxp+OwhD2QhiSNmNCzoDb0EeUXTD2n/WbcnrAYMGlmf05th6QYq23VqvJqpA==", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">=18.0.0" @@ -776,6 +1634,15 @@ "node": ">=12" } }, + "node_modules/@google/generative-ai": { + "version": "0.24.1", + "resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.24.1.tgz", + "integrity": "sha512-MqO+MLfM6kjxcKoy0p1wRzG3b4ZZXtPI+z2IE26UogS2Cm/XHO+7gGRBh6gcJsOiIVoH93UwKvW4HdgiOZCy9Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@hapi/address": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/@hapi/address/-/address-5.1.1.tgz", @@ -852,6 +1719,59 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "license": "MIT" + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.22.0.tgz", + "integrity": "sha512-VUpl106XVTCpDmTBil2ehgJZjhyLY2QZikzF8NvTXtLRF1CvO5iEE2UNZdVIUer35vFOwMKYeUGbjJtvPWan3g==", + "license": "MIT", + "dependencies": { + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + } + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/express-rate-limit": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, "node_modules/@mongodb-js/saslprep": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.3.2.tgz", @@ -861,14 +1781,45 @@ "sparse-bitfield": "^3.0.3" } }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, "node_modules/@smithy/abort-controller": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.4.tgz", - "integrity": "sha512-Z4DUr/AkgyFf1bOThW2HwzREagee0sB5ycl+hDiSZOfRLW8ZgrOjDi6g8mHH19yyU5E2A/64W3z6SMIf5XiUSQ==", - "dev": true, + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.5.tgz", + "integrity": "sha512-j7HwVkBw68YW8UmFRcjZOmssE77Rvk0GWAIN1oFBhsaovQmZWYCIcGa9/pwRB0ExI8Sk9MWNALTjftjHZea7VA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/chunked-blob-reader": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-5.2.0.tgz", + "integrity": "sha512-WmU0TnhEAJLWvfSeMxBNe5xtbselEO8+4wG0NtZeL8oR21WgH1xiO37El+/Y+H/Ie4SCwBy3MxYWmOYaGgZueA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/chunked-blob-reader-native": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-4.2.1.tgz", + "integrity": "sha512-lX9Ay+6LisTfpLid2zZtIhSEjHMZoAR5hHCR4H7tBz/Zkfr5ea8RcQ7Tk4mi0P76p4cN+Btz16Ffno7YHpKXnQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" }, "engines": { @@ -876,17 +1827,16 @@ } }, "node_modules/@smithy/config-resolver": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.2.tgz", - "integrity": "sha512-4Jys0ni2tB2VZzgslbEgszZyMdTkPOFGA8g+So/NjR8oy6Qwaq4eSwsrRI+NMtb0Dq4kqCzGUu/nGUx7OM/xfw==", - "dev": true, + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.3.tgz", + "integrity": "sha512-ezHLe1tKLUxDJo2LHtDuEDyWXolw8WGOR92qb4bQdWq/zKenO5BvctZGrVJBK08zjezSk7bmbKFOXIVyChvDLw==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.4", - "@smithy/types": "^4.8.1", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/types": "^4.9.0", "@smithy/util-config-provider": "^4.2.0", - "@smithy/util-endpoints": "^3.2.4", - "@smithy/util-middleware": "^4.2.4", + "@smithy/util-endpoints": "^3.2.5", + "@smithy/util-middleware": "^4.2.5", "tslib": "^2.6.2" }, "engines": { @@ -894,19 +1844,18 @@ } }, "node_modules/@smithy/core": { - "version": "3.17.2", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.17.2.tgz", - "integrity": "sha512-n3g4Nl1Te+qGPDbNFAYf+smkRVB+JhFsGy9uJXXZQEufoP4u0r+WLh6KvTDolCswaagysDc/afS1yvb2jnj1gQ==", - "dev": true, + "version": "3.18.3", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.18.3.tgz", + "integrity": "sha512-qqpNskkbHOSfrbFbjhYj5o8VMXO26fvN1K/+HbCzUNlTuxgNcPRouUDNm+7D6CkN244WG7aK533Ne18UtJEgAA==", "license": "Apache-2.0", "dependencies": { - "@smithy/middleware-serde": "^4.2.4", - "@smithy/protocol-http": "^5.3.4", - "@smithy/types": "^4.8.1", + "@smithy/middleware-serde": "^4.2.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-middleware": "^4.2.4", - "@smithy/util-stream": "^4.5.5", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-stream": "^4.5.6", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" @@ -916,16 +1865,85 @@ } }, "node_modules/@smithy/credential-provider-imds": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.4.tgz", - "integrity": "sha512-YVNMjhdz2pVto5bRdux7GMs0x1m0Afz3OcQy/4Yf9DH4fWOtroGH7uLvs7ZmDyoBJzLdegtIPpXrpJOZWvUXdw==", - "dev": true, + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.5.tgz", + "integrity": "sha512-BZwotjoZWn9+36nimwm/OLIcVe+KYRwzMjfhd4QT7QxPm9WY0HiOV8t/Wlh+HVUif0SBVV7ksq8//hPaBC/okQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.4", - "@smithy/property-provider": "^4.2.4", - "@smithy/types": "^4.8.1", - "@smithy/url-parser": "^4.2.4", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-codec": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.5.tgz", + "integrity": "sha512-Ogt4Zi9hEbIP17oQMd68qYOHUzmH47UkK7q7Gl55iIm9oKt27MUGrC5JfpMroeHjdkOliOA4Qt3NQ1xMq/nrlA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.9.0", + "@smithy/util-hex-encoding": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-browser": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.5.tgz", + "integrity": "sha512-HohfmCQZjppVnKX2PnXlf47CW3j92Ki6T/vkAT2DhBR47e89pen3s4fIa7otGTtrVxmj7q+IhH0RnC5kpR8wtw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.2.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-config-resolver": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.5.tgz", + "integrity": "sha512-ibjQjM7wEXtECiT6my1xfiMH9IcEczMOS6xiCQXoUIYSj5b1CpBbJ3VYbdwDy8Vcg5JHN7eFpOCGk8nyZAltNQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-node": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.5.tgz", + "integrity": "sha512-+elOuaYx6F2H6x1/5BQP5ugv12nfJl66GhxON8+dWVUEDJ9jah/A0tayVdkLRP0AeSac0inYkDz5qBFKfVp2Gg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.2.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-universal": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.5.tgz", + "integrity": "sha512-G9WSqbST45bmIFaeNuP/EnC19Rhp54CcVdX9PDL1zyEB514WsDVXhlyihKlGXnRycmHNmVv88Bvvt4EYxWef/Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-codec": "^4.2.5", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -933,15 +1951,14 @@ } }, "node_modules/@smithy/fetch-http-handler": { - "version": "5.3.5", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.5.tgz", - "integrity": "sha512-mg83SM3FLI8Sa2ooTJbsh5MFfyMTyNRwxqpKHmE0ICRIa66Aodv80DMsTQI02xBLVJ0hckwqTRr5IGAbbWuFLQ==", - "dev": true, + "version": "5.3.6", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.6.tgz", + "integrity": "sha512-3+RG3EA6BBJ/ofZUeTFJA7mHfSYrZtQIrDP9dI8Lf7X6Jbos2jptuLrAAteDiFVrmbEmLSuRG/bUKzfAXk7dhg==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.4", - "@smithy/querystring-builder": "^4.2.4", - "@smithy/types": "^4.8.1", + "@smithy/protocol-http": "^5.3.5", + "@smithy/querystring-builder": "^4.2.5", + "@smithy/types": "^4.9.0", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" }, @@ -949,14 +1966,28 @@ "node": ">=18.0.0" } }, + "node_modules/@smithy/hash-blob-browser": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.2.6.tgz", + "integrity": "sha512-8P//tA8DVPk+3XURk2rwcKgYwFvwGwmJH/wJqQiSKwXZtf/LiZK+hbUZmPj/9KzM+OVSwe4o85KTp5x9DUZTjw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/chunked-blob-reader": "^5.2.0", + "@smithy/chunked-blob-reader-native": "^4.2.1", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@smithy/hash-node": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.4.tgz", - "integrity": "sha512-kKU0gVhx/ppVMntvUOZE7WRMFW86HuaxLwvqileBEjL7PoILI8/djoILw3gPQloGVE6O0oOzqafxeNi2KbnUJw==", - "dev": true, + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.5.tgz", + "integrity": "sha512-DpYX914YOfA3UDT9CN1BM787PcHfWRBB43fFGCYrZFUH0Jv+5t8yYl+Pd5PW4+QzoGEDvn5d5QIO4j2HyYZQSA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.8.1", + "@smithy/types": "^4.9.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" @@ -965,14 +1996,27 @@ "node": ">=18.0.0" } }, + "node_modules/@smithy/hash-stream-node": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.2.5.tgz", + "integrity": "sha512-6+do24VnEyvWcGdHXomlpd0m8bfZePpUKBy7m311n+JuRwug8J4dCanJdTymx//8mi0nlkflZBvJe+dEO/O12Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@smithy/invalid-dependency": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.4.tgz", - "integrity": "sha512-z6aDLGiHzsMhbS2MjetlIWopWz//K+mCoPXjW6aLr0mypF+Y7qdEh5TyJ20Onf9FbWHiWl4eC+rITdizpnXqOw==", - "dev": true, + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.5.tgz", + "integrity": "sha512-2L2erASEro1WC5nV+plwIMxrTXpvpfzl4e+Nre6vBVRR2HKeGGcvpJyyL3/PpiSg+cJG2KpTmZmq934Olb6e5A==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.8.1", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -983,7 +2027,6 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", - "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -992,15 +2035,28 @@ "node": ">=18.0.0" } }, + "node_modules/@smithy/md5-js": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.2.5.tgz", + "integrity": "sha512-Bt6jpSTMWfjCtC0s79gZ/WZ1w90grfmopVOWqkI2ovhjpD5Q2XRXuecIPB9689L2+cCySMbaXDhBPU56FKNDNg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@smithy/middleware-content-length": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.4.tgz", - "integrity": "sha512-hJRZuFS9UsElX4DJSJfoX4M1qXRH+VFiLMUnhsWvtOOUWRNvvOfDaUSdlNbjwv1IkpVjj/Rd/O59Jl3nhAcxow==", - "dev": true, + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.5.tgz", + "integrity": "sha512-Y/RabVa5vbl5FuHYV2vUCwvh/dqzrEY/K2yWPSqvhFUwIY0atLqO4TienjBXakoy4zrKAMCZwg+YEqmH7jaN7A==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.4", - "@smithy/types": "^4.8.1", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -1008,19 +2064,18 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.3.6.tgz", - "integrity": "sha512-PXehXofGMFpDqr933rxD8RGOcZ0QBAWtuzTgYRAHAL2BnKawHDEdf/TnGpcmfPJGwonhginaaeJIKluEojiF/w==", - "dev": true, + "version": "4.3.10", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.3.10.tgz", + "integrity": "sha512-SoAag3QnWBFoXjwa1jenEThkzJYClidZUyqsLKwWZ8kOlZBwehrLBp4ygVDjNEM2a2AamCQ2FBA/HuzKJ/LiTA==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.17.2", - "@smithy/middleware-serde": "^4.2.4", - "@smithy/node-config-provider": "^4.3.4", - "@smithy/shared-ini-file-loader": "^4.3.4", - "@smithy/types": "^4.8.1", - "@smithy/url-parser": "^4.2.4", - "@smithy/util-middleware": "^4.2.4", + "@smithy/core": "^3.18.3", + "@smithy/middleware-serde": "^4.2.5", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", + "@smithy/url-parser": "^4.2.5", + "@smithy/util-middleware": "^4.2.5", "tslib": "^2.6.2" }, "engines": { @@ -1028,19 +2083,18 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "4.4.6", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.6.tgz", - "integrity": "sha512-OhLx131znrEDxZPAvH/OYufR9d1nB2CQADyYFN4C3V/NQS7Mg4V6uvxHC/Dr96ZQW8IlHJTJ+vAhKt6oxWRndA==", - "dev": true, + "version": "4.4.10", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.10.tgz", + "integrity": "sha512-6fOwX34gXxcqKa3bsG0mR0arc2Cw4ddOS6tp3RgUD2yoTrDTbQ2aVADnDjhUuxaiDZN2iilxndgGDhnpL/XvJA==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.4", - "@smithy/protocol-http": "^5.3.4", - "@smithy/service-error-classification": "^4.2.4", - "@smithy/smithy-client": "^4.9.2", - "@smithy/types": "^4.8.1", - "@smithy/util-middleware": "^4.2.4", - "@smithy/util-retry": "^4.2.4", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/service-error-classification": "^4.2.5", + "@smithy/smithy-client": "^4.9.6", + "@smithy/types": "^4.9.0", + "@smithy/util-middleware": "^4.2.5", + "@smithy/util-retry": "^4.2.5", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" }, @@ -1049,14 +2103,13 @@ } }, "node_modules/@smithy/middleware-serde": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.4.tgz", - "integrity": "sha512-jUr3x2CDhV15TOX2/Uoz4gfgeqLrRoTQbYAuhLS7lcVKNev7FeYSJ1ebEfjk+l9kbb7k7LfzIR/irgxys5ZTOg==", - "dev": true, + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.5.tgz", + "integrity": "sha512-La1ldWTJTZ5NqQyPqnCNeH9B+zjFhrNoQIL1jTh4zuqXRlmXhxYHhMtI1/92OlnoAtp6JoN7kzuwhWoXrBwPqg==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.4", - "@smithy/types": "^4.8.1", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -1064,13 +2117,12 @@ } }, "node_modules/@smithy/middleware-stack": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.4.tgz", - "integrity": "sha512-Gy3TKCOnm9JwpFooldwAboazw+EFYlC+Bb+1QBsSi5xI0W5lX81j/P5+CXvD/9ZjtYKRgxq+kkqd/KOHflzvgA==", - "dev": true, + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.5.tgz", + "integrity": "sha512-bYrutc+neOyWxtZdbB2USbQttZN0mXaOyYLIsaTbJhFsfpXyGWUxJpEuO1rJ8IIJm2qH4+xJT0mxUSsEDTYwdQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.8.1", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -1078,15 +2130,14 @@ } }, "node_modules/@smithy/node-config-provider": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.4.tgz", - "integrity": "sha512-3X3w7qzmo4XNNdPKNS4nbJcGSwiEMsNsRSunMA92S4DJLLIrH5g1AyuOA2XKM9PAPi8mIWfqC+fnfKNsI4KvHw==", - "dev": true, + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.5.tgz", + "integrity": "sha512-UTurh1C4qkVCtqggI36DGbLB2Kv8UlcFdMXDcWMbqVY2uRg0XmT9Pb4Vj6oSQ34eizO1fvR0RnFV4Axw4IrrAg==", "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.2.4", - "@smithy/shared-ini-file-loader": "^4.3.4", - "@smithy/types": "^4.8.1", + "@smithy/property-provider": "^4.2.5", + "@smithy/shared-ini-file-loader": "^4.4.0", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -1094,16 +2145,15 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.4.tgz", - "integrity": "sha512-VXHGfzCXLZeKnFp6QXjAdy+U8JF9etfpUXD1FAbzY1GzsFJiDQRQIt2CnMUvUdz3/YaHNqT3RphVWMUpXTIODA==", - "dev": true, + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.5.tgz", + "integrity": "sha512-CMnzM9R2WqlqXQGtIlsHMEZfXKJVTIrqCNoSd/QpAyp+Dw0a1Vps13l6ma1fH8g7zSPNsA59B/kWgeylFuA/lw==", "license": "Apache-2.0", "dependencies": { - "@smithy/abort-controller": "^4.2.4", - "@smithy/protocol-http": "^5.3.4", - "@smithy/querystring-builder": "^4.2.4", - "@smithy/types": "^4.8.1", + "@smithy/abort-controller": "^4.2.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/querystring-builder": "^4.2.5", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -1111,13 +2161,12 @@ } }, "node_modules/@smithy/property-provider": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.4.tgz", - "integrity": "sha512-g2DHo08IhxV5GdY3Cpt/jr0mkTlAD39EJKN27Jb5N8Fb5qt8KG39wVKTXiTRCmHHou7lbXR8nKVU14/aRUf86w==", - "dev": true, + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.5.tgz", + "integrity": "sha512-8iLN1XSE1rl4MuxvQ+5OSk/Zb5El7NJZ1td6Tn+8dQQHIjp59Lwl6bd0+nzw6SKm2wSSriH2v/I9LPzUic7EOg==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.8.1", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -1125,13 +2174,12 @@ } }, "node_modules/@smithy/protocol-http": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.4.tgz", - "integrity": "sha512-3sfFd2MAzVt0Q/klOmjFi3oIkxczHs0avbwrfn1aBqtc23WqQSmjvk77MBw9WkEQcwbOYIX5/2z4ULj8DuxSsw==", - "dev": true, + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.5.tgz", + "integrity": "sha512-RlaL+sA0LNMp03bf7XPbFmT5gN+w3besXSWMkA8rcmxLSVfiEXElQi4O2IWwPfxzcHkxqrwBFMbngB8yx/RvaQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.8.1", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -1139,13 +2187,12 @@ } }, "node_modules/@smithy/querystring-builder": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.4.tgz", - "integrity": "sha512-KQ1gFXXC+WsbPFnk7pzskzOpn4s+KheWgO3dzkIEmnb6NskAIGp/dGdbKisTPJdtov28qNDohQrgDUKzXZBLig==", - "dev": true, + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.5.tgz", + "integrity": "sha512-y98otMI1saoajeik2kLfGyRp11e5U/iJYH/wLCh3aTV/XutbGT9nziKGkgCaMD1ghK7p6htHMm6b6scl9JRUWg==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.8.1", + "@smithy/types": "^4.9.0", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" }, @@ -1154,13 +2201,12 @@ } }, "node_modules/@smithy/querystring-parser": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.4.tgz", - "integrity": "sha512-aHb5cqXZocdzEkZ/CvhVjdw5l4r1aU/9iMEyoKzH4eXMowT6M0YjBpp7W/+XjkBnY8Xh0kVd55GKjnPKlCwinQ==", - "dev": true, + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.5.tgz", + "integrity": "sha512-031WCTdPYgiQRYNPXznHXof2YM0GwL6SeaSyTH/P72M1Vz73TvCNH2Nq8Iu2IEPq9QP2yx0/nrw5YmSeAi/AjQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.8.1", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -1168,26 +2214,24 @@ } }, "node_modules/@smithy/service-error-classification": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.4.tgz", - "integrity": "sha512-fdWuhEx4+jHLGeew9/IvqVU/fxT/ot70tpRGuOLxE3HzZOyKeTQfYeV1oaBXpzi93WOk668hjMuuagJ2/Qs7ng==", - "dev": true, + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.5.tgz", + "integrity": "sha512-8fEvK+WPE3wUAcDvqDQG1Vk3ANLR8Px979te96m84CbKAjBVf25rPYSzb4xU4hlTyho7VhOGnh5i62D/JVF0JQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.8.1" + "@smithy/types": "^4.9.0" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/shared-ini-file-loader": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.3.4.tgz", - "integrity": "sha512-y5ozxeQ9omVjbnJo9dtTsdXj9BEvGx2X8xvRgKnV+/7wLBuYJQL6dOa/qMY6omyHi7yjt1OA97jZLoVRYi8lxA==", - "dev": true, + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.0.tgz", + "integrity": "sha512-5WmZ5+kJgJDjwXXIzr1vDTG+RhF9wzSODQBfkrQ2VVkYALKGvZX1lgVSxEkgicSAFnFhPj5rudJV0zoinqS0bA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.8.1", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -1195,17 +2239,16 @@ } }, "node_modules/@smithy/signature-v4": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.4.tgz", - "integrity": "sha512-ScDCpasxH7w1HXHYbtk3jcivjvdA1VICyAdgvVqKhKKwxi+MTwZEqFw0minE+oZ7F07oF25xh4FGJxgqgShz0A==", - "dev": true, + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.5.tgz", + "integrity": "sha512-xSUfMu1FT7ccfSXkoLl/QRQBi2rOvi3tiBZU2Tdy3I6cgvZ6SEi9QNey+lqps/sJRnogIS+lq+B1gxxbra2a/w==", "license": "Apache-2.0", "dependencies": { "@smithy/is-array-buffer": "^4.2.0", - "@smithy/protocol-http": "^5.3.4", - "@smithy/types": "^4.8.1", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", "@smithy/util-hex-encoding": "^4.2.0", - "@smithy/util-middleware": "^4.2.4", + "@smithy/util-middleware": "^4.2.5", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" @@ -1215,18 +2258,17 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "4.9.2", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.9.2.tgz", - "integrity": "sha512-gZU4uAFcdrSi3io8U99Qs/FvVdRxPvIMToi+MFfsy/DN9UqtknJ1ais+2M9yR8e0ASQpNmFYEKeIKVcMjQg3rg==", - "dev": true, + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.9.6.tgz", + "integrity": "sha512-hGz42hggqReicRRZUvrKDQiAmoJnx1Q+XfAJnYAGu544gOfxQCAC3hGGD7+Px2gEUUxB/kKtQV7LOtBRNyxteQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.17.2", - "@smithy/middleware-endpoint": "^4.3.6", - "@smithy/middleware-stack": "^4.2.4", - "@smithy/protocol-http": "^5.3.4", - "@smithy/types": "^4.8.1", - "@smithy/util-stream": "^4.5.5", + "@smithy/core": "^3.18.3", + "@smithy/middleware-endpoint": "^4.3.10", + "@smithy/middleware-stack": "^4.2.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "@smithy/util-stream": "^4.5.6", "tslib": "^2.6.2" }, "engines": { @@ -1234,10 +2276,9 @@ } }, "node_modules/@smithy/types": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.8.1.tgz", - "integrity": "sha512-N0Zn0OT1zc+NA+UVfkYqQzviRh5ucWwO7mBV3TmHHprMnfcJNfhlPicDkBHi0ewbh+y3evR6cNAW0Raxvb01NA==", - "dev": true, + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.9.0.tgz", + "integrity": "sha512-MvUbdnXDTwykR8cB1WZvNNwqoWVaTRA0RLlLmf/cIFNMM2cKWz01X4Ly6SMC4Kks30r8tT3Cty0jmeWfiuyHTA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -1247,14 +2288,13 @@ } }, "node_modules/@smithy/url-parser": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.4.tgz", - "integrity": "sha512-w/N/Iw0/PTwJ36PDqU9PzAwVElo4qXxCC0eCTlUtIz/Z5V/2j/cViMHi0hPukSBHp4DVwvUlUhLgCzqSJ6plrg==", - "dev": true, + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.5.tgz", + "integrity": "sha512-VaxMGsilqFnK1CeBX+LXnSuaMx4sTL/6znSZh2829txWieazdVxr54HmiyTsIbpOTLcf5nYpq9lpzmwRdxj6rQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/querystring-parser": "^4.2.4", - "@smithy/types": "^4.8.1", + "@smithy/querystring-parser": "^4.2.5", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -1265,7 +2305,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.0.tgz", "integrity": "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/util-buffer-from": "^4.2.0", @@ -1280,7 +2319,6 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.0.tgz", "integrity": "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==", - "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -1293,7 +2331,6 @@ "version": "4.2.1", "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.1.tgz", "integrity": "sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA==", - "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -1306,7 +2343,6 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz", "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/is-array-buffer": "^4.2.0", @@ -1320,7 +2356,6 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.0.tgz", "integrity": "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==", - "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -1330,15 +2365,14 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.5.tgz", - "integrity": "sha512-GwaGjv/QLuL/QHQaqhf/maM7+MnRFQQs7Bsl6FlaeK6lm6U7mV5AAnVabw68cIoMl5FQFyKK62u7RWRzWL25OQ==", - "dev": true, + "version": "4.3.9", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.9.tgz", + "integrity": "sha512-Bh5bU40BgdkXE2BcaNazhNtEXi1TC0S+1d84vUwv5srWfvbeRNUKFzwKQgC6p6MXPvEgw+9+HdX3pOwT6ut5aw==", "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.2.4", - "@smithy/smithy-client": "^4.9.2", - "@smithy/types": "^4.8.1", + "@smithy/property-provider": "^4.2.5", + "@smithy/smithy-client": "^4.9.6", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -1346,18 +2380,17 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.8.tgz", - "integrity": "sha512-gIoTf9V/nFSIZ0TtgDNLd+Ws59AJvijmMDYrOozoMHPJaG9cMRdqNO50jZTlbM6ydzQYY8L/mQ4tKSw/TB+s6g==", - "dev": true, + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.12.tgz", + "integrity": "sha512-EHZwe1E9Q7umImIyCKQg/Cm+S+7rjXxCRvfGmKifqwYvn7M8M4ZcowwUOQzvuuxUUmdzCkqL0Eq0z1m74Pq6pw==", "license": "Apache-2.0", "dependencies": { - "@smithy/config-resolver": "^4.4.2", - "@smithy/credential-provider-imds": "^4.2.4", - "@smithy/node-config-provider": "^4.3.4", - "@smithy/property-provider": "^4.2.4", - "@smithy/smithy-client": "^4.9.2", - "@smithy/types": "^4.8.1", + "@smithy/config-resolver": "^4.4.3", + "@smithy/credential-provider-imds": "^4.2.5", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/property-provider": "^4.2.5", + "@smithy/smithy-client": "^4.9.6", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -1365,14 +2398,13 @@ } }, "node_modules/@smithy/util-endpoints": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.2.4.tgz", - "integrity": "sha512-f+nBDhgYRCmUEDKEQb6q0aCcOTXRDqH5wWaFHJxt4anB4pKHlgGoYP3xtioKXH64e37ANUkzWf6p4Mnv1M5/Vg==", - "dev": true, + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.2.5.tgz", + "integrity": "sha512-3O63AAWu2cSNQZp+ayl9I3NapW1p1rR5mlVHcF6hAB1dPZUQFfRPYtplWX/3xrzWthPGj5FqB12taJJCfH6s8A==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.4", - "@smithy/types": "^4.8.1", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -1383,7 +2415,6 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.0.tgz", "integrity": "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -1393,13 +2424,12 @@ } }, "node_modules/@smithy/util-middleware": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.4.tgz", - "integrity": "sha512-fKGQAPAn8sgV0plRikRVo6g6aR0KyKvgzNrPuM74RZKy/wWVzx3BMk+ZWEueyN3L5v5EDg+P582mKU+sH5OAsg==", - "dev": true, + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.5.tgz", + "integrity": "sha512-6Y3+rvBF7+PZOc40ybeZMcGln6xJGVeY60E7jy9Mv5iKpMJpHgRE6dKy9ScsVxvfAYuEX4Q9a65DQX90KaQ3bA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.8.1", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -1407,14 +2437,13 @@ } }, "node_modules/@smithy/util-retry": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.4.tgz", - "integrity": "sha512-yQncJmj4dtv/isTXxRb4AamZHy4QFr4ew8GxS6XLWt7sCIxkPxPzINWd7WLISEFPsIan14zrKgvyAF+/yzfwoA==", - "dev": true, + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.5.tgz", + "integrity": "sha512-GBj3+EZBbN4NAqJ/7pAhsXdfzdlznOh8PydUijy6FpNIMnHPSMO2/rP4HKu+UFeikJxShERk528oy7GT79YiJg==", "license": "Apache-2.0", "dependencies": { - "@smithy/service-error-classification": "^4.2.4", - "@smithy/types": "^4.8.1", + "@smithy/service-error-classification": "^4.2.5", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -1422,15 +2451,14 @@ } }, "node_modules/@smithy/util-stream": { - "version": "4.5.5", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.5.tgz", - "integrity": "sha512-7M5aVFjT+HPilPOKbOmQfCIPchZe4DSBc1wf1+NvHvSoFTiFtauZzT+onZvCj70xhXd0AEmYnZYmdJIuwxOo4w==", - "dev": true, + "version": "4.5.6", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.6.tgz", + "integrity": "sha512-qWw/UM59TiaFrPevefOZ8CNBKbYEP6wBAIlLqxn3VAIo9rgnTNc4ASbVrqDmhuwI87usnjhdQrxodzAGFFzbRQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/fetch-http-handler": "^5.3.5", - "@smithy/node-http-handler": "^4.4.4", - "@smithy/types": "^4.8.1", + "@smithy/fetch-http-handler": "^5.3.6", + "@smithy/node-http-handler": "^4.4.5", + "@smithy/types": "^4.9.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", @@ -1445,7 +2473,6 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.0.tgz", "integrity": "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==", - "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -1458,7 +2485,6 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/util-buffer-from": "^4.2.0", @@ -1468,11 +2494,24 @@ "node": ">=18.0.0" } }, + "node_modules/@smithy/util-waiter": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.5.tgz", + "integrity": "sha512-Dbun99A3InifQdIrsXZ+QLcC0PGBPAdrl4cj1mTgJvyc9N2zf7QSxg8TBkzsCmGJdE3TLbO9ycwpY0EkWahQ/g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.2.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@smithy/uuid": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.0.tgz", "integrity": "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -1487,6 +2526,85 @@ "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", "license": "MIT" }, + "node_modules/@supabase/auth-js": { + "version": "2.81.0", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.81.0.tgz", + "integrity": "sha512-mWyRPO+XUo19MHNBFg5qdH8cMIyxRNj9HXhwkwToxDHYRZWru96hWZFCVb7trOrTpPVe4TgLer2yy3KMvYBMPw==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.81.0", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.81.0.tgz", + "integrity": "sha512-yxxIGbXm1TtRpP5VwXKEZIdQMd2XUrWS1xt3zPF3jMItX5dXfdpbz5YRPY3IfebR8gXB113d/APWvYLiNuzI1Q==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/postgrest-js": { + "version": "2.81.0", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.81.0.tgz", + "integrity": "sha512-HdybTRf5Sy+gBxzgwkag+WkvV8QqMXhnKQ383YG51lCbm8p82CuCcUTzGy2xFHiA2ZXnnlkSzrfw8uKFAiAiog==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.81.0", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.81.0.tgz", + "integrity": "sha512-WCL9kMbmHQNGAG4ep+jfU22+h9OiQVv7bbkOmLy4gwlqtE+SJszkAtRp3l3xthqYkbxHbIqGc/BlHv3Dh79cXg==", + "license": "MIT", + "dependencies": { + "@types/phoenix": "^1.6.6", + "@types/ws": "^8.18.1", + "tslib": "2.8.1", + "ws": "^8.18.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.81.0", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.81.0.tgz", + "integrity": "sha512-gj9u+EyEVLgDA9jW8JOsAgEc8H79zg01STK5KLv9EU45kf5Qh7kAoCmG090Jkp/YEGvSiaR/Ta7Xs/gUTLqflw==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.81.0", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.81.0.tgz", + "integrity": "sha512-FkiqUYCzsT92V/mfvoFueszkQrPqSTHgXhN9ADqeMpY5j0tUqeAZu8g2ptLYiDmx1pBbh4xoiqxWAf3UDIv4Bw==", + "license": "MIT", + "dependencies": { + "@supabase/auth-js": "2.81.0", + "@supabase/functions-js": "2.81.0", + "@supabase/postgrest-js": "2.81.0", + "@supabase/realtime-js": "2.81.0", + "@supabase/storage-js": "2.81.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", @@ -1568,7 +2686,6 @@ "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.5.tgz", "integrity": "sha512-LuIQOcb6UmnF7C1PCFmEU1u2hmiHL43fgFQX67sN3H4Z+0Yk0Neo++mFsBjhOAuLzvlQeqAAkeDOZrJs9rzumQ==", "license": "MIT", - "peer": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", @@ -1593,6 +2710,12 @@ "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", "license": "MIT" }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, "node_modules/@types/jsonwebtoken": { "version": "9.0.10", "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", @@ -1619,18 +2742,36 @@ "mongoose": "*" } }, + "node_modules/@types/morgan": { + "version": "1.9.10", + "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.10.tgz", + "integrity": "sha512-sS4A1zheMvsADRVfT0lYbJ4S9lmsey8Zo2F7cnbYjWHP67Q0AwMYuuzLlkIM2N8gAbb9cubhIVFwcIN2XyYCkA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", "license": "MIT" }, + "node_modules/@types/multer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.0.0.tgz", + "integrity": "sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/node": { "version": "24.10.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.0.tgz", "integrity": "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -1686,6 +2827,12 @@ "@types/passport": "*" } }, + "node_modules/@types/phoenix": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz", + "integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==", + "license": "MIT" + }, "node_modules/@types/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", @@ -1728,6 +2875,24 @@ "@types/node": "*" } }, + "node_modules/@types/swagger-jsdoc": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/swagger-jsdoc/-/swagger-jsdoc-6.0.4.tgz", + "integrity": "sha512-W+Xw5epcOZrF/AooUM/PccNMSAFOKWZA5dasNyMujTwsBkU74njSJBpvCCJhHAJ95XRMzQrrW844Btu0uoetwQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/swagger-ui-express": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.8.tgz", + "integrity": "sha512-AhZV8/EIreHFmBV5wAs0gzJUNq9JbbSXgJLQubCC0jtIo6prnI9MIRRxnU4MZX9RB9yXxF1V4R7jtLl/Wcj31g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/serve-static": "*" + } + }, "node_modules/@types/webidl-conversions": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", @@ -1743,6 +2908,15 @@ "@types/webidl-conversions": "*" } }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -1782,6 +2956,39 @@ "node": ">=0.4.0" } }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -1796,6 +3003,12 @@ "node": ">= 8" } }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -1803,11 +3016,16 @@ "dev": true, "license": "MIT" }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, "license": "MIT" }, "node_modules/base64url": { @@ -1819,6 +3037,24 @@ "node": ">=6.0.0" } }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/bcrypt": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", @@ -1870,14 +3106,12 @@ "version": "2.12.1", "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.12.1.tgz", "integrity": "sha512-z4rE2Gxh7tvshQ4hluIT7XcFrgLIQaw9X3A+kTTRdovCz5PMukm/0QC/BKSYPj3omF5Qfypn9O/c5kgpmvYUCw==", - "dev": true, "license": "MIT" }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -1912,6 +3146,23 @@ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", "license": "BSD-3-Clause" }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -1950,6 +3201,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-me-maybe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", + "license": "MIT" + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -1975,13 +3232,36 @@ "fsevents": "~2.3.2" } }, + "node_modules/commander": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz", + "integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, "license": "MIT" }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, "node_modules/content-disposition": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", @@ -2060,6 +3340,41 @@ "dev": true, "license": "MIT" }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csv-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/csv-parser/-/csv-parser-3.2.0.tgz", + "integrity": "sha512-fgKbp+AJbn1h2dcAHKIdKNSSjfp43BZZykXsCjzALjKy80VXQNHPFJ6T9Afwdzoj24aMkq8GwDS7KGcDPpejrA==", + "license": "MIT", + "bin": { + "csv-parser": "bin/csv-parser" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -2096,6 +3411,18 @@ "node": ">=0.3.1" } }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/dotenv": { "version": "17.2.3", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", @@ -2182,6 +3509,15 @@ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -2191,6 +3527,27 @@ "node": ">= 0.6" } }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/express": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", @@ -2233,11 +3590,50 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-rate-limit": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz", + "integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==", + "license": "MIT", + "dependencies": { + "ip-address": "10.0.1" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fast-xml-parser": { "version": "5.2.5", "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", - "dev": true, "funding": [ { "type": "github", @@ -2252,6 +3648,29 @@ "fxparser": "src/cli/cli.js" } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -2282,6 +3701,18 @@ "node": ">= 0.8" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -2300,6 +3731,12 @@ "node": ">= 0.8" } }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2361,6 +3798,27 @@ "node": ">= 0.4" } }, + "node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", @@ -2464,12 +3922,32 @@ "dev": true, "license": "ISC" }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "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.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -2531,6 +4009,12 @@ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, "node_modules/joi": { "version": "18.0.1", "resolved": "https://registry.npmjs.org/joi/-/joi-18.0.1.tgz", @@ -2549,6 +4033,24 @@ "node": ">= 20" } }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, "node_modules/jsonwebtoken": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", @@ -2601,6 +4103,13 @@ "node": ">=12.0.0" } }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", + "license": "MIT" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -2613,6 +4122,13 @@ "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", "license": "MIT" }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, "node_modules/lodash.isinteger": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", @@ -2637,6 +4153,12 @@ "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", "license": "MIT" }, + "node_modules/lodash.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", + "license": "MIT" + }, "node_modules/lodash.once": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", @@ -2711,7 +4233,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -2720,6 +4241,27 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/mongodb": { "version": "6.20.0", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz", @@ -2782,20 +4324,63 @@ "integrity": "sha512-ww2T4dBV+suCbOfG5YPwj9pLCfUVyj8FEA1D3Ux1HHqutpLxGyOYEPU06iPRBW4cKr3PJfOSYsIpHWPTkz5zig==", "license": "MIT", "dependencies": { - "bson": "^6.10.4", - "kareem": "2.6.3", - "mongodb": "~6.20.0", - "mpath": "0.9.0", - "mquery": "5.0.0", - "ms": "2.1.3", - "sift": "17.1.3" + "bson": "^6.10.4", + "kareem": "2.6.3", + "mongodb": "~6.20.0", + "mpath": "0.9.0", + "mquery": "5.0.0", + "ms": "2.1.3", + "sift": "17.1.3" + }, + "engines": { + "node": ">=16.20.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mongoose" + } + }, + "node_modules/morgan": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", + "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==", + "license": "MIT", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.1.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/morgan/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" }, "engines": { - "node": ">=16.20.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mongoose" + "node": ">= 0.8" } }, "node_modules/mpath": { @@ -2825,6 +4410,67 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/multer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", + "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "mkdirp": "^0.5.6", + "object-assign": "^4.1.1", + "type-is": "^1.6.18", + "xtend": "^4.0.2" + }, + "engines": { + "node": ">= 10.16.0" + } + }, + "node_modules/multer/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/negotiator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", @@ -2843,6 +4489,44 @@ "node": "^18 || ^20 || >= 21" } }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "node_modules/node-gyp-build": { "version": "4.8.4", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", @@ -2941,6 +4625,15 @@ "node": ">= 0.8" } }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -2950,6 +4643,13 @@ "wrappy": "1" } }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "license": "MIT", + "peer": true + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -3017,6 +4717,24 @@ "node": ">= 0.4.0" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-to-regexp": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", @@ -3045,6 +4763,31 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkce-challenge": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", + "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -3129,6 +4872,20 @@ "url": "https://opencollective.com/express" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -3142,6 +4899,15 @@ "node": ">=8.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -3239,6 +5005,27 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -3339,6 +5126,12 @@ "memory-pager": "^1.0.2" } }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -3348,6 +5141,23 @@ "node": ">= 0.8" } }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/stripe": { "version": "19.2.1", "resolved": "https://registry.npmjs.org/stripe/-/stripe-19.2.1.tgz", @@ -3372,7 +5182,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", - "dev": true, "funding": [ { "type": "github", @@ -3394,6 +5203,62 @@ "node": ">=4" } }, + "node_modules/swagger-jsdoc": { + "version": "6.2.8", + "resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz", + "integrity": "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==", + "license": "MIT", + "dependencies": { + "commander": "6.2.0", + "doctrine": "3.0.0", + "glob": "7.1.6", + "lodash.mergewith": "^4.6.2", + "swagger-parser": "^10.0.3", + "yaml": "2.0.0-1" + }, + "bin": { + "swagger-jsdoc": "bin/swagger-jsdoc.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/swagger-parser": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz", + "integrity": "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==", + "license": "MIT", + "dependencies": { + "@apidevtools/swagger-parser": "10.0.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/swagger-ui-dist": { + "version": "5.30.2", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.30.2.tgz", + "integrity": "sha512-HWCg1DTNE/Nmapt+0m2EPXFwNKNeKK4PwMjkwveN/zn1cV2Kxi9SURd+m0SpdcSgWEK/O64sf8bzXdtUhigtHA==", + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, + "node_modules/swagger-ui-express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", + "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", + "license": "MIT", + "dependencies": { + "swagger-ui-dist": ">=5.0.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -3486,7 +5351,6 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, "license": "0BSD" }, "node_modules/type-is": { @@ -3503,13 +5367,18 @@ "node": ">= 0.6" } }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -3546,6 +5415,12 @@ "node": ">= 0.8" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -3562,6 +5437,15 @@ "dev": true, "license": "MIT" }, + "node_modules/validator": { + "version": "13.15.23", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.23.tgz", + "integrity": "sha512-4yoz1kEWqUjzi5zsPbAS/903QXSYp0UOtHsPpp7p9rHAw/W+dkInskAE386Fat3oKRROwO98d9ZB0G4cObgUyw==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -3571,6 +5455,15 @@ "node": ">= 0.8" } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", @@ -3593,12 +5486,89 @@ "node": ">=18" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yaml": { + "version": "2.0.0-1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz", + "integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/yamljs": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/yamljs/-/yamljs-0.3.0.tgz", + "integrity": "sha512-C/FsVVhht4iPQYXOInoxUM/1ELSf9EsgKH34FofQOp6hwCPrW4vG4w5++TED3xRUo8gD7l0P1J1dLlDYzODsTQ==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "glob": "^7.0.5" + }, + "bin": { + "json2yaml": "bin/json2yaml", + "yaml2json": "bin/yaml2json" + } + }, + "node_modules/yamljs/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", @@ -3608,6 +5578,54 @@ "engines": { "node": ">=6" } + }, + "node_modules/z-schema": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", + "integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==", + "license": "MIT", + "dependencies": { + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "validator": "^13.7.0" + }, + "bin": { + "z-schema": "bin/z-schema" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "commander": "^9.4.1" + } + }, + "node_modules/z-schema/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.24.6", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", + "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.24.1" + } } } } diff --git a/package.json b/package.json index ed7813f..680c20e 100644 --- a/package.json +++ b/package.json @@ -14,32 +14,49 @@ "author": "", "license": "ISC", "dependencies": { + "@aws-sdk/client-s3": "^3.930.0", + "@google/generative-ai": "^0.24.1", + "@modelcontextprotocol/sdk": "^1.22.0", + "@supabase/supabase-js": "^2.81.0", "@types/bcrypt": "^6.0.0", "@types/jsonwebtoken": "^9.0.10", + "@types/morgan": "^1.9.10", "@types/passport": "^1.0.17", "@types/passport-google-oauth20": "^2.0.17", "bcrypt": "^6.0.0", "body-parser": "^2.2.0", "cookie-parser": "^1.4.7", "cors": "^2.8.5", + "csv-parser": "^3.2.0", "dotenv": "^17.2.3", "express": "^5.1.0", + "express-rate-limit": "^8.2.1", "joi": "^18.0.1", "jsonwebtoken": "^9.0.2", "mongoose": "^8.19.1", + "morgan": "^1.10.1", + "multer": "^2.0.2", + "node-fetch": "^3.3.2", "nodemailer": "^7.0.10", "passport": "^0.7.0", "passport-google-oauth20": "^2.0.0", - "stripe": "^19.2.1" + "stripe": "^19.2.1", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1", + "yamljs": "^0.3.0" }, "devDependencies": { "@types/cookie-parser": "^1.4.10", "@types/cors": "^2.8.19", "@types/express": "^5.0.5", "@types/mongoose": "^5.11.96", + "@types/multer": "^2.0.0", "@types/node": "^24.10.0", "@types/nodemailer": "^7.0.3", + "@types/swagger-jsdoc": "^6.0.4", + "@types/swagger-ui-express": "^4.1.8", "nodemon": "^3.1.10", + "prettier": "^3.6.2", "ts-node": "^10.9.2", "typescript": "^5.9.3" } diff --git a/prettier.config.js b/prettier.config.js new file mode 100644 index 0000000..36a390d --- /dev/null +++ b/prettier.config.js @@ -0,0 +1,43 @@ +/** @type {import("prettier").Config} */ +const config = { + // Format options + printWidth: 100, + tabWidth: 2, + useTabs: false, + semi: true, + singleQuote: false, + trailingComma: 'all', + bracketSpacing: true, + bracketSameLine: false, + arrowParens: 'avoid', + htmlWhitespaceSensitivity: 'css', + endOfLine: 'lf', + proseWrap: 'always', + quoteProps: 'as-needed', + embeddedLanguageFormatting: 'auto', + + // Overrides for specific file types + overrides: [ + { + files: ['*.json', '*.json5', '*.yaml', '*.yml'], + options: { + tabWidth: 2, + singleQuote: false, + trailingComma: 'none', + }, + }, + { + files: ['*.md', '*.mdx'], + options: { + printWidth: 150, + proseWrap: 'preserve', + }, + }, + { + files: ['*.ts', '*.tsx'], + options: {}, + }, + ], +}; + +export default config; diff --git a/src/controllers/UserController.ts b/src/admincontroller/activityController.ts similarity index 100% rename from src/controllers/UserController.ts rename to src/admincontroller/activityController.ts diff --git a/src/admincontroller/insightsController.ts b/src/admincontroller/insightsController.ts new file mode 100644 index 0000000..512571c --- /dev/null +++ b/src/admincontroller/insightsController.ts @@ -0,0 +1,16 @@ +import { Response, Request} from "express"; +import { deviceModel } from "../model/admin/insights/deviceModel"; + +export const getUserDevices = async (req: Request, res: Response) => { + try { + const user = (req as any).user; // get logged user-info from request + + // find all devices from db where userId matches logged in user + const devices = await deviceModel + .find({ userId: user._id }) + .sort({ createdAt: -1 }); + res.json({devices}) + } catch (err) { + res.status(500).json({ message: "Error Fetching Devices", error: err }); + } +}; diff --git a/src/admincontroller/userController.ts b/src/admincontroller/userController.ts new file mode 100644 index 0000000..040c6f1 --- /dev/null +++ b/src/admincontroller/userController.ts @@ -0,0 +1,102 @@ +import { Request,Response } from "express"; +import userModel from "../model/user";//called userdatabase +import guestModel from "../model/guest"; + + +//function for get allloged users count to admindashboard graph +export const loggedusers = async(req:Request,res:Response)=>{ + +try{ + +const fetcchallusers = await userModel.find().countDocuments() +res.status(200).json({message:"user count taken",fetcchallusers,success:true}) + +} +catch{ + + res.status(500).json({message:"user not found"}) +}} + + + +//function for get new logged users count per month +export const getNewUsersPerMonth = async (req: Request, res: Response) => { + try { + const usersPerMonth = await userModel.aggregate([ + { + $group: { + _id: { + year: { $year: "$createdAt" }, + month: { $month: "$createdAt" } + }, + count: { $sum: 1 } + } + }, + { + $sort: { + "_id.year": 1, + "_id.month": 1 + } + } + ]); + res.status(200).json({message: "new users per month count get successfully",success: true,usersPerMonth}); + } catch { + + res.status(500).json({message:"new user per month count not get"}) + + } +}; + + + + +//function for add guestusers +export const addGustuser = async(req:Request,res:Response)=>{ + try { + const newGuest = await guestModel.create({ + token: 12345, + IP_address: 19216801 + }); + res.status(200).json({message:"guest user added successfully",addGustuser,success:true}) +} + +catch{ + +res.status(500).json({message:"guest user not added some error"}) +} +} + + +//function for get full gustusers count +export const fetchAllgustusers = async(req:Request,res:Response)=>{ + try{ + const getAllgustusers = await guestModel.find().countDocuments() + res.status(200).json({message:"all gust users fetched successfully",allgustusers:getAllgustusers,success:true}) + + } + catch{ + + } +} + + + +//function for get all users count +export const getAllusers = async (req: Request, res: Response) => { + try { + const loggedCount = await userModel.countDocuments(); + const guestCount = await guestModel.countDocuments(); + + const totalCount = loggedCount + guestCount; + + res.status(200).json({ + message: "All users fetched successfully", + totalCount, + loggedCount, + guestCount, + success: true, + }); + } catch (err) { + res.status(500).json({ message: "failed to fetch all users", error: err }); + } +}; diff --git a/src/app.ts b/src/app.ts index 263730c..af54ce3 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,51 +1,137 @@ -import express from 'express'; -import dotenv from 'dotenv' -dotenv.config() -import mongoose from 'mongoose' -import cors from 'cors'; -import passport from './config/passport.ts'; +import express from "express"; +import dotenv from "dotenv"; +import mongoose from "mongoose"; +import morgan from "morgan"; +import cors from "cors"; +import passport from "./config/passport.ts"; import cookieParser from "cookie-parser"; -import googleRouter from './routes/authRoutes.ts'; -import uploadRouter from './routes/uploadRouter.ts' -import userRouter from './routes/userRouter.ts'; -import paymentRouter from './routes/paymentRoutes.js'; +import rateLimit from "express-rate-limit"; +import swaggerJSDoc from "swagger-jsdoc"; +import swaggerUi from "swagger-ui-express"; +import authRouter from "./routes/authRoutes.ts"; +import uploadRouter from "./routes/uploadRouter.ts"; +import userRouter from "./routes/userRouter.ts"; +import sessionRouter from "./routes/sessionRoutes.ts" +import paymentRouter from "./routes/paymentRoutes.js"; +import { adminrouter } from "./routes/adminroutes/userRouter.ts"; +import { insightsRouter } from "./routes/adminroutes/insightsRouter.ts"; +import { fileSizeCheck } from "./middlewares/fileSizeCheck.ts"; +import chatRouter from "./routes/chatRouter.ts"; -const app = express() -app.use(express.json()) -app.use(express.urlencoded({ extended: true })) -app.use(passport.initialize()) -app.use(cookieParser()) +dotenv.config(); +const app = express(); +app.use(express.json({ limit: "50mb" })); +app.use(express.urlencoded({ extended: true })); +app.use(passport.initialize()); +app.use(cookieParser()); +app.use(morgan("dev")); +const swaggerOptions = { + definition: { + openapi: "3.0.0", + info: { + title: "InstaviZ Api documentation", + version: "1.0.0", + description: "API Documentation", + }, + servers: [ + { + url: `http://localhost:${process.env.PORT}`, + }, + ], + + components: { + securitySchemes: { + bearerAuth: { + type: "http", + scheme: "bearer", + bearerFormat: "JWT", + }, + }, + }, + + security: [ + { + bearerAuth: [], + }, + ], + }, + + apis: ["./src/routes/**/*.{ts,js}"], +}; + + +const swaggerSpec = swaggerJSDoc(swaggerOptions); +app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerSpec)); + +// rate limiter per request +const limiter = rateLimit({ + windowMs: 5 * 60 * 1000, + limit: 220, + standardHeaders: true, + legacyHeaders: false, +}); //database connection const connection = async () => { try { await mongoose.connect(process.env.mongo_uri!); console.log("Mongoose connected"); + } catch (err) { + console.log(err); } - catch (err) { - console.log(err) - } -} +}; + +app.use( + cors({ + origin: (origin, callback) => { + const allowedOrigins = [ + process.env.CLIENT_URL, + `http://localhost:${process.env.PORT}` + ]; + + if (!origin || allowedOrigins.includes(origin)) { + callback(null, true); + } else { + callback(new Error("Not allowed by CORS")); + } + }, + credentials: true, + allowedHeaders: ["Content-Type", "Authorization","x-session-id"], + }) +); -app.use(cors({ - origin: process.env.CLIENT_URL, - credentials: true -})); + +app.use(limiter); app.use("/payment/webhook", express.raw({ type: "application/json" })); //middleware -app.use("/upload", uploadRouter) +app.use("/upload", fileSizeCheck, uploadRouter) app.use("/user", userRouter) -app.use("/auth", googleRouter) -app.use("/payment",paymentRouter) +app.use("/auth", authRouter) +app.use("/payment", paymentRouter) +app.use("/chat",chatRouter) +app.use("/session",sessionRouter) +// admin routes +app.use("/admin/dashboard", adminrouter); +app.use("/admin/dashboard", insightsRouter); -//routing -// app.use +//file upload check +app.use(fileSizeCheck); -//listening -app.listen(process.env.PORT,() => { - connection(); +//listening +app.listen(process.env.PORT, async () => { + await connection(); console.log(` Server running on http://localhost:${process.env.PORT}`); -}); \ No newline at end of file +}); +app.get("/health", async (req, res) => { + const dbStatus = mongoose.connection.readyState === 1 ? "connected" : "disconnected"; + + res.json({ + status: 'ok', + db: dbStatus, + uptime: process.uptime(), + time: new Date().toISOString(), + }); +}); diff --git a/src/auth/auth.ts b/src/auth/auth.ts new file mode 100644 index 0000000..4761c68 --- /dev/null +++ b/src/auth/auth.ts @@ -0,0 +1,158 @@ +import { Request, Response } from "express"; +import bcrypt from "bcrypt"; +import otpModel from "../model/otpModel.ts"; +import userModel from "../model/user.ts"; +import { sendOtp } from "../utils/sendEmail.ts"; +import { generateOtp } from "../utils/otpGenerate.ts"; +import Jwt from "jsonwebtoken"; +import { loginSchema } from "../services/validation.ts"; +import { signJwt } from "../services/jwtServices.ts"; +import refreshModel from "../model/refreshtoken"; +import { hashToken } from "../utils/hashTokens.ts"; +import { theValidation } from "../services/validation.ts"; + +export const loginCheck = async (req: Request, res: Response) => { + try { + const { email, password } = req.body; + if (!email || !password) { + return res.status(400).json({ + success: false, + message: "Email and password are required", + }); + } + const { error } = loginSchema.validate(req.body, { abortEarly: false }); + if (error) { + const details = error.details.map(err => err.message); + return res.status(400).json({ success: false, message: details }); + } + + const user = await userModel.findOne({ email }); + + if (!user) { + return res.status(404).json({ + success: false, + message: "User not found. Please register first.", + }); + } + + if (!user.password) { + return res.status(400).json({ + success: false, + message: "This account was created using Google. Please login with Google.", + }); + } + + const isPasswordCorrect = await bcrypt.compare(password, user.password!); + if (!isPasswordCorrect) { + return res.status(401).json({ + success: false, + message: "Invalid email or password", + }); + } + const accessToken = signJwt({ id: user._id, email: user.email }); + const refreshToken = Jwt.sign( + { + id: user._id, + email: user.email, + }, + process.env.REFRESH_SECRET!, + { expiresIn: "30d" }, + ); + + const hashed = hashToken(refreshToken); + const userAgent = req.headers["user-agent"] || "unknown"; + const ip = (req.headers["x-forwarded-for"] as string)?.split(",")[0] || req.ip || "unknown"; + + const session = await refreshModel.create({ + userId: user._id, + tokenhash: hashed, + expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), + userAgent, + ip, + createdAt: new Date(), + lastActiveAt: new Date(), + isValid: true, + }); + res.cookie("refreshToken", refreshToken, { + httpOnly: true, + secure: false, + sameSite: "strict", + maxAge: 30 * 24 * 60 * 60 * 1000, + }); + + return res.status(200).json({ + success: true, + message: "Login successful", + accessToken, + sessionId: session._id, + user: { + id: user._id, + name: user.name, + email: user.email, + }, + }); + } catch (err) { + return res.status(500).json({ + success: false, + message: "Internal server error", + }); + } +}; + + +export const register = async (req: Request, res: Response) => { + try { + const { name, email, password, confirmPassword } = req.body; + + const { error } = theValidation.validate(req.body, { abortEarly: false }); + + if (error) { + const details = error.details.map(err => err.message); + return res.status(400).json({ + success: false, + message: "Validation failed", + errors: details, + }); + } + const existUser = await userModel.findOne({ email }); + + if (existUser) { + return res.status(400).json({ message: "user already exists" }); + } + + const otp = generateOtp(); + await otpModel.create({ name, password, email, otp }); + + await sendOtp(email, otp); + + res.status(200).json({ message: "plz verify the otp to continue", otp: true }); + } catch (err) { + console.error("Register Error", err); + res.status(500).json({ message: "someting went wrong", success: false }); + } +}; + + +export const getAllSessions = async (req: Request, res: Response) => { + try { + interface JwtUser { + id: string; + email: string; + } + const user = req.user as JwtUser; + const userId = user.id; + + const sessions = await refreshModel.find({ userId, isValid: true }).select("-tokenhash"); + + return res.status(200).json({ + success: true, + sessions, + }); + } catch (err) { + console.error("Get sessions error:", err); + return res.status(500).json({ + success: false, + message: "Internal server error", + }); + } +}; diff --git a/src/auth/googleAuth.ts b/src/auth/googleAuth.ts new file mode 100644 index 0000000..494d38f --- /dev/null +++ b/src/auth/googleAuth.ts @@ -0,0 +1,59 @@ +import { Request, Response } from "express"; +import Jwt from "jsonwebtoken"; +import refreshModel from "../model/refreshtoken"; +import { hashToken } from "../utils/hashTokens"; + +export const googleCallback = async (req: Request, res: Response) => { + try { + const user = req.user as any; + + const redirect = typeof req.query.state === "string" ? req.query.state : "/home"; + const accessToken = Jwt.sign( + { + id: user._id.toString(), + email: user.email, + googleId: user.googleId?.toString() || null, + }, + process.env.JWT_SECRET!, + { expiresIn: "15m" }, + ); + + const refreshToken = Jwt.sign({ id: user._id.toString() }, process.env.REFRESH_SECRET!, { + expiresIn: "30d", + }); + + const userAgent = req.headers["user-agent"] || "unknown"; + const ip = (req.headers["x-forwarded-for"] as string)?.split(",")[0] || req.ip || "unknown"; + const session = await refreshModel + .findOneAndUpdate( + { userId: user._id }, + { + userId: user._id, + tokenhash: hashToken(refreshToken), + userAgent, + ip, + createdAt: new Date(), + lastActiveAt: new Date(), + expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), + isValid: true, + }, + { upsert: true, new: true }, + ) + .select("_id"); + + res.cookie("refreshToken", refreshToken, { + httpOnly: true, + secure: false, + sameSite: "strict", + maxAge: 30 * 24 * 60 * 60 * 1000, + }); + + const frontendURL = process.env.CLIENT_URL!; + res.redirect( + `${frontendURL}/auth/callback?token=${accessToken}&sessionId=${session._id}&redirect=${redirect}`, + ); + } catch (err) { + console.log("Google OAuth error:", err); + res.status(500).json({ message: "Google auth failed" }); + } +}; diff --git a/src/auth/logoutAuth.ts b/src/auth/logoutAuth.ts new file mode 100644 index 0000000..cfe60e6 --- /dev/null +++ b/src/auth/logoutAuth.ts @@ -0,0 +1,85 @@ +import { Request, Response } from "express"; +import refreshModel from '../model/refreshtoken' + +export const logoutDevice = async (req: Request, res: Response) => { + try { + const { sessionId } = req.body; + interface JwtUser { + id: string; + email: string; + } + const user = req.user as JwtUser; + const userId = user.id; + + if (!sessionId) { + return res.status(400).json({ success: false, message: "Session ID required" }); + } + + const session = await refreshModel.findOne({ + _id: sessionId, + userId, + }); + + if (!session) { + return res.status(404).json({ success: false, message: "Session not found" }); + } + + await refreshModel.deleteOne({ _id: sessionId }); + res.clearCookie("refreshToken", { + httpOnly: true, + secure: false, + sameSite: "strict", + }); + + res.clearCookie("userId", { + httpOnly: true, + secure: false, + sameSite: "strict", + }); + return res.json({ + success: true, + message: "Device logged out successfully", + }); + } catch (err) { + console.error("Logout Device Error", err); + return res.status(500).json({ + success: false, + message: "Internal server error", + }); + } +}; + +export const logoutAllDevices = async (req: Request, res: Response) => { + console.log("reached here at logout all devices"); + try { + interface JwtUser { + id: string; + email: string; + } + + const user = req.user as JwtUser; + const userId = user.id; + + const { currentSessionId } = req.body; + if (!currentSessionId) { + return res.status(400).json({ success: false, message: "Current session ID required" }); + } + + await refreshModel.deleteMany({ + userId, + _id: { $ne: currentSessionId }, + }); + + + return res.json({ + success: true, + message: "Logged out from all other devices", + }); + } catch (err) { + console.error("Logout All Devices Error", err); + return res.status(500).json({ + success: false, + message: "Internal server error", + }); + } +}; diff --git a/src/auth/otp.ts b/src/auth/otp.ts new file mode 100644 index 0000000..9a66d6e --- /dev/null +++ b/src/auth/otp.ts @@ -0,0 +1,154 @@ +import type { Request, Response } from "express"; +import bcrypt from "bcrypt"; +import otpModel from "../model/otpModel.ts"; +import userModel from "../model/user.ts"; +import { sendOtp } from "../utils/sendEmail.ts"; +import { generateOtp } from "../utils/otpGenerate.ts"; +import Jwt from "jsonwebtoken"; +import { signJwt } from "../services/jwtServices.ts"; +import guestModel from "../model/guest.ts"; + + +export const verifyOtp = async (req: Request, res: Response) => { + console.log("reached here at verify otp"); + try { + const { email } = req.query; + const { otp } = req.body; + console.log(`${email},${otp}`); + const otpData = await otpModel.findOne({ email }); + console.log("before logging otp data"); + + console.log(otpData); + + console.log("after loging otp data"); + + if (!otpData) { + console.log("no otp data"); + + return res.status(400).json({ message: "otp not found" }); + } + if (otpData.otp.toString() !== otp.toString()) { + console.log("otp mismatch"); + + return res.status(400).json({ message: "Invalid otp" }); + } + + const hashedPassword = await bcrypt.hash(otpData.password, 10); + console.log("checking user cookie :", req.cookies); + if (req.cookies.userId && req.cookies.isGuest == 'true') { + const guestUser = await guestModel.findById(req.cookies.userId); + const existingUserToken = guestUser?.token; + await guestModel.findByIdAndDelete(req.cookies.userId) + const createUser = await userModel.create({ + _id: req.cookies.userId, + token: existingUserToken, + name: otpData.name, + email: otpData.email, + password: hashedPassword, + }); + await otpModel.deleteOne({ email }); + res.clearCookie("isGuest", { + httpOnly: true, + secure: false, + sameSite: "strict", + maxAge: 30 * 24 * 60 * 60 * 1000, + }); + return res.status(200).json({ + success: true, + message: "Registration completed successfully" + }); + } + console.log("there is no guest:userId on cookie", req.cookies); + const createUser = await userModel.create({ + name: otpData.name, + email: otpData.email, + password: hashedPassword, + }); + console.log("after create user"); + res.cookie("userId", createUser._id, { + httpOnly: true, + secure: false, + sameSite: "strict", + maxAge: 30 * 24 * 60 * 60 * 1000, + }); + // res.cookie("isGuest", false, { + // httpOnly: true, + // secure: false, + // sameSite: "strict", + // maxAge: 30 * 24 * 60 * 60 * 1000, + // }); + await otpModel.deleteOne({ email }); + + return res.status(200).json({ + success: true, + message: "Registration completed successfully" + }); + } catch (err) { + console.log("veryfyotp catch worked", err); + res.status(500).json({ message: "Internal server errror", error: err }); + } +}; + +export const resendOtp = async (req: Request, res: Response) => { + try { + const email = req.query.email?.toString(); + + if (!email) { + return res.status(400).json({ message: "Email is required" }); + } + + const otpData = await otpModel.findOne({ email }); + + if (!otpData) { + return res.status(400).json({ message: "No OTP found for this email" }); + } + + const otp = generateOtp(); + + await sendOtp(email, otp); + + await otpModel.findOneAndUpdate( + { email }, + { + otp, + createdAt: new Date(), + }, + ); + + return res.status(200).json({ + success: true, + message: "OTP resent successfully", + }); + } catch (err) { + console.log(err); + return res.status(500).json({ message: "Internal server error" }); + } +}; + +export const verifyForgotOtp = async (req: Request, res: Response) => { + console.log("reached here at verify otp"); + console.log(req.body); + + try { + const { email, otp } = req.body; + + if (!email || !otp) { + return res.status(400).json({ success: false, message: "Email & OTP required" }); + } + + const savedOtp = await otpModel.findOne({ email }); + + if (!savedOtp) { + return res.status(400).json({ success: false, message: "OTP not found" }); + } + + if (savedOtp.otp.toString() !== otp.toString()) { + return res.status(400).json({ success: false, message: "Invalid OTP" }); + } + + return res.json({ success: true, message: "OTP verified successfully" }); + } catch (err) { + console.error("OTP Verify Error", err); + return res.status(500).json({ success: false, message: "Internal server error" }); + } +}; \ No newline at end of file diff --git a/src/auth/password.ts b/src/auth/password.ts new file mode 100644 index 0000000..048e453 --- /dev/null +++ b/src/auth/password.ts @@ -0,0 +1,69 @@ +import type { Request, Response } from "express"; +import bcrypt from "bcrypt"; +import otpModel from "../model/otpModel.ts"; +import userModel from "../model/user.ts"; +import { sendOtp } from "../utils/sendEmail.ts"; +import { generateOtp } from "../utils/otpGenerate.ts"; +import { resetPasswordSchema, theValidation } from "../services/validation.ts"; + + +export const forgotPassword = async (req: Request, res: Response) => { + console.log("reached here at forgot password"); + console.log(req.body); + + try { + const { email } = req.body; + + if (!email) { + return res.status(400).json({ success: false, message: "Email is required" }); + } + //comment + const user = await userModel.findOne({ email }); + if (!user) { + return res.status(404).json({ success: false, message: "User not found" }); + } + + const otp = generateOtp(); + console.log("otp sentt for forgott", otp); + + await otpModel.findOneAndUpdate({ email }, { otp }, { upsert: true }); + + await sendOtp(email, otp); + + return res.json({ + success: true, + message: "OTP has been sent to your email", + }); + } catch (err) { + console.error("Forgot Password Error", err); + return res.status(500).json({ success: false, message: "Internal server error" }); + } +}; + +export const resetPassword = async (req: Request, res: Response) => { + try { + const { email, newPassword, confirmPassword } = req.body; + + const { error } = resetPasswordSchema.validate(req.body, { abortEarly: false }); + if (error) { + const details = error.details.map(err => err.message); + return res.status(400).json({ success: false, message: details }); + } + + const otpData = await otpModel.findOne({ email }); + + const hashed = await bcrypt.hash(newPassword, 10); + + await userModel.findOneAndUpdate({ email }, { password: hashed }); + + await otpModel.deleteOne({ email }); + + return res.json({ + success: true, + message: "Password has been reset successfully", + }); + } catch (err) { + console.error("Reset Password Error", err); + return res.status(500).json({ success: false, message: "Internal server error" }); + } +}; \ No newline at end of file diff --git a/src/config/mcpTools.ts b/src/config/mcpTools.ts new file mode 100644 index 0000000..9a67d7b --- /dev/null +++ b/src/config/mcpTools.ts @@ -0,0 +1,56 @@ +import type { FunctionDeclaration } from "@google/generative-ai"; +import { SchemaType } from "@google/generative-ai"; + +export const mcpToolDeclarations: FunctionDeclaration[] = [ + { + name: "aggregate", + description: "Execute MongoDB aggregation pipeline to compute chart data", + parameters: { + type: SchemaType.OBJECT, + properties: { + database: { + type: SchemaType.STRING, + description: "Database name" + }, + collection: { + type: SchemaType.STRING, + description: "Collection name" + }, + pipeline: { + type: SchemaType.ARRAY, + description: "MongoDB aggregation pipeline stages", + items: { + type: SchemaType.OBJECT, + description: "Single pipeline stage", + properties: {} // <-- REQUIRED but valid empty object + } + } + }, + required: ["database", "collection", "pipeline"] + } + }, + + { + name: "find", + description: "Query MongoDB collection to fetch documents", + parameters: { + type: SchemaType.OBJECT, + properties: { + database: { + type: SchemaType.STRING, + description: "Database name" + }, + collection: { + type: SchemaType.STRING, + description: "Collection name" + }, + query: { + type: SchemaType.OBJECT, + description: "MongoDB query filter", + properties: {} // <-- empty but valid shape + } + }, + required: ["database", "collection", "query"] + } + } +]; diff --git a/src/config/r2Client.ts b/src/config/r2Client.ts new file mode 100644 index 0000000..57870cb --- /dev/null +++ b/src/config/r2Client.ts @@ -0,0 +1,10 @@ +import { S3Client } from "@aws-sdk/client-s3"; + +export const r2 = new S3Client({ + region: "auto", + endpoint: `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`, + credentials: { + accessKeyId: process.env.R2_ACCESS_KEY_ID!, + secretAccessKey: process.env.R2_SECRET_ACCESS_KEY! + } +}); diff --git a/src/config/supabaseClient.ts b/src/config/supabaseClient.ts new file mode 100644 index 0000000..bb98f9b --- /dev/null +++ b/src/config/supabaseClient.ts @@ -0,0 +1,6 @@ +import { createClient } from "@supabase/supabase-js"; + +const supebaseUrl = process.env.SUPABASE_URL!; +const supabaseSuperKey = process.env.SUPABASE_SERVICE_ROLE_KEY!; + +export const supabase = createClient(supebaseUrl, supabaseSuperKey); \ No newline at end of file diff --git a/src/controller/auth/auth.ts b/src/controller/auth/auth.ts deleted file mode 100644 index 591e06e..0000000 --- a/src/controller/auth/auth.ts +++ /dev/null @@ -1,221 +0,0 @@ -import type { Request, Response } from "express"; -import bcrypt from "bcrypt" -import otpModel from "../../model/otpModel.ts"; -import Joi, { number } from "../../../node_modules/joi/lib/index"; -import userModel from "../../model/user.ts"; -import { sendOtp } from "../../utils/sendEmail.ts"; -import { theValidation } from "../../services/validation.ts"; -import { generateOtp } from "../../utils/otpGenerate.ts"; -import Jwt from "jsonwebtoken"; -import { loginSchema } from "../../services/validation.ts"; - - -// google authentication -import { signJwt } from "../../services/jwtServices.ts"; -import type { User } from "../../model/user.ts"; -import { json } from "body-parser"; - -export const loginCheck = async (req: Request, res: Response) => { - console.log(" reached here login"); - console.log(req.body); - - - try { - const { email, password } = req.body; - if(!email || !password){ - return res.status(400).json({ - success: false, - message: "Email and password are required", - }); - } - const { error} = loginSchema.validate(req.body,{abortEarly:false}) - if(error){ - const details = error.details.map((err)=>err.message) - return res.status(400).json({success:false,message:"validation failed"}) - } - - const user = await userModel.findOne({ email }); - if (!user) { - return res.status(404).json({ - success: false, - message: "User not found. Please register first.", - }); - } - - const isPasswordCorrect = await bcrypt.compare(password, user.password!); - if (!isPasswordCorrect) { - return res.status(401).json({ - success: false, - message: "Invalid email or password", - }); - } - - const accessToken = signJwt({id:user._id,email:user.email}) - - const refreshToken = Jwt.sign({ - id:user._id,email:user.email, - }, process.env.REFRESH_SECRET!, { expiresIn: '30d' }); - - - res.cookie("refreshToken",refreshToken, - { - httpOnly:true, - secure:false, - sameSite:"strict", - maxAge:30*24*60*60*1000, - - } - ) - - - - return res.status(200).json({ - success: true, - message: "Login successful", - accessToken, - user: { - id: user._id, - name: user.name, - email: user.email, - }, - }); - - - - - } catch (err) { - console.log("catch in login worked"); - - console.error("Login error:", err); - return res.status(500).json({ - success: false, - message: "Internal server error", - }); - } -}; - - - -// dummy bro - -export const getAllUser = async (req: Request, res: Response) => { - try { - const user = await userModel.find(); - res.status(200).json({ message: "All users", user }) - } - catch (err) { - res.status(500).json(err) - } -} - -export const register = async (req: Request, res: Response) => { - - try { - const { name, email, password, confirmPassword } = req.body; - - const { error } = theValidation.validate(req.body, { abortEarly: false }); - - if (error) { - const details = error.details.map((err) => err.message); - return res.status(400).json({ - success: false, - message: "Validation failed", - errors: details, - }); - } - const existUser = await userModel.findOne({ email }); - - if (existUser) { - return res.status(400).json({ message: "user already exists" }); - } - - const otp = generateOtp(); - console.log(otp); - - - - const createOtpDoc = await otpModel.create({ name, password, email, otp }); - - await sendOtp(email, otp); - - - res.status(200).json({ message: "plz verify the otp to continue", otp: true }); - } catch (err) { - console.error("Register Error", err); - res.status(500).json({ message: "someting went wrong", success: false }); - } -}; - -export const verifyOtp = async (req: Request, res: Response) => { - console.log("reached here at verify otp"); - try { - const { email } = req.query; - const { otp } = req.body; - console.log(`${email},${otp}`) - const otpData = await otpModel.findOne({ email }); - console.log("before logging otp data"); - - console.log(otpData); - - console.log("after loging otp data"); - - - if (!otpData) { - console.log("no otp data"); - - return res.status(400).json({ message: "otp not found" }); - } - if (otpData.otp.toString() !== otp.toString()) { - console.log("otp mismatch"); - - return res.status(400).json({ message: "Invalid otp" }); - } - - const hashedPassword = await bcrypt.hash(otpData.password, 10) - console.log("after hashed pass"); - - const createUser = await userModel.create({ name: otpData.name, email: otpData.email, password: hashedPassword }); - console.log("after create user"); - - console.log(createUser); - - console.log("after loging create logging"); - - await otpModel.deleteOne({ email }); - - - - return res.status(200).json({ - success: true, - message: "Registration completed successfully", - }); - - } catch (err) { - console.log("veryfyotp catch woerked", err); - res.status(500).json({ message: "Internal server errror" }); - } -}; - - -// google authentication - -export const googleCallback = (req: Request, res: Response) => { - const user = req.user as User; - - const token = signJwt({ - id: user.googleId, - email: user.email - }); - const frontendURL = "http://localhost:3000"; - res.redirect(`${frontendURL}/auth/callback?token=${token}`); -}; - - -export const testpro = (req:Request,res:Response)=>{ -try{ - return res.json({message:"reached protecteed route"}) - -}catch(err){ - return res.json({message:"error",error:err}) -} -} diff --git a/src/controllers/chatController.ts b/src/controllers/chatController.ts new file mode 100644 index 0000000..bdd4ba2 --- /dev/null +++ b/src/controllers/chatController.ts @@ -0,0 +1,73 @@ +import type { Request, Response } from "express"; +import Dataset from "../model/dataModel"; +import { userWantsChart } from "../utils/chatDetection"; +import { createChatPrompt } from "../utils/chatPrompt"; +import { runChartAnalysis } from "../services/chatAiService"; +import { modelLight } from "../services/aiModels"; +import chatModel from "../model/chat"; +import mongoose from "mongoose"; + +export const chatController = async (req: Request, res: Response) => { + try { + const { message } = req.body; + // Ensure user is authenticated + if (!req.cookies.userId) { + return res.status(401).json({ + reply: "Please log in or continue as guest.", + chart: null, + }); + } + + console.log("req.cookies.userId also in userId:", req.cookies.userId) + const userId = req.cookies.userId; + // latest dataset for user + const dataset = await Dataset.findOne({ user_id: userId }) + .sort({ created_at: -1 }); + + if (!dataset) { + return res.json({ + reply: "Please upload a dataset first.", + chart: null, + }); + } + + // If chart requested use Heavy model + if (userWantsChart(message)) { + const result = await runChartAnalysis(message, dataset); + return res.json({ message: "chart generated based on chat", result, success: true }); + } + + // Otherwise use Cheap model + const prompt = createChatPrompt(message, dataset); + const chat = modelLight.startChat({ history: [] }); + + const reply = await chat.sendMessage(prompt); + // console.log("reply for chat", reply.response.text()) + console.log("userid :", userId) + const uploadChat = async () => { + try { + const user_id = new mongoose.Types.ObjectId(userId) + await chatModel.updateOne( + { user_id: user_id }, + { $push: { chat: { fromAi: reply.response.text(), fromUser: message } } } + ); + + } + catch (err) { + console.log("error while uploading the chat to mongodb:", err) + } + } + uploadChat() + return res.json({ + reply: reply.response.text(), + chart: null, + }); + + } catch (err) { + console.error("error occured in chat response with ai:", err); + return res.json({ + reply: "Something went wrong. Try again.", + chart: null, + }); + } +}; diff --git a/src/controllers/paymentController.ts b/src/controllers/paymentController.ts index 14fcdb8..e555ec6 100644 --- a/src/controllers/paymentController.ts +++ b/src/controllers/paymentController.ts @@ -1,6 +1,8 @@ import { Request, Response } from "express"; -import { stripe } from "../config/stripe.js"; -import { CheckoutRequestBody } from "../types/paymentTypes.js"; +import { stripe } from "../config/stripe.ts"; +import { CheckoutRequestBody } from "../types/paymentTypes.ts"; +import userModel from "../model/user.ts"; +import { token } from "morgan"; const priceMap: Record = { Starter: 15, @@ -16,7 +18,9 @@ export const createCheckoutSession = async ( const { plan } = req.body; if (!priceMap[plan]) { - res.status(400).json({ message: "Invalid plan selected",success:false}); + res + .status(400) + .json({ message: "Invalid plan selected", success: false }); return; } @@ -36,16 +40,42 @@ export const createCheckoutSession = async ( success_url: `${process.env.CLIENT_URL}/success`, cancel_url: `${process.env.CLIENT_URL}/cancel`, }); + console.log("plan that user take:", plan,"!plan"); + let user = await userModel.findById(req.cookies.userId); - res.status(200).json({ url: session.url ,message:"checkout created",success:true}); + if (!user) { + res.status(404).json({ message: "User not found", success: false }); + return; + } + if (plan === 'Starter') { + user.token = (user.token ?? 0) + 3; + await user.save(); + } + if (plan === 'Pro') { + user.token = (user.token ?? 0) + 7; + await user.save(); + } + + if (plan === 'Enterprise') { + user.token = (user.token ?? 0) + 10; + await user.save(); + } + res + .status(200) + .json({ url: session.url, message: "checkout created", success: true }); } catch (error: any) { console.error("Stripe Error (createCheckoutSession):", error); - res.status(500).json({message: error.message || "Something went wrong creating session",success:false}); + res.status(500).json({ + message: error.message || "Something went wrong creating session", + success: false, + }); } - }; -export const handleWebhook = async (req: Request, res: Response): Promise => { +export const handleWebhook = async ( + req: Request, + res: Response +): Promise => { const sig = req.headers["stripe-signature"]; let event; @@ -57,7 +87,9 @@ export const handleWebhook = async (req: Request, res: Response): Promise ); } catch (err: any) { console.error("Webhook Error:", err.message); - res.status(400).json({message:`Webhook Error: ${err.message}`,success:false}); + res + .status(400) + .json({ message: `Webhook Error: ${err.message}`, success: false }); return; } @@ -66,5 +98,5 @@ export const handleWebhook = async (req: Request, res: Response): Promise console.log("Payment successful:", session); } - res.json({ received: true ,message:"payment received",success:true}); + res.json({ received: true, message: "payment received", success: true }); }; diff --git a/src/controllers/sessionController.ts b/src/controllers/sessionController.ts new file mode 100644 index 0000000..5b1e8b9 --- /dev/null +++ b/src/controllers/sessionController.ts @@ -0,0 +1,230 @@ +// controllers/sessionController.ts +import { Request, Response } from "express"; +import crypto from "crypto"; +import mongoose from "mongoose"; +import { SessionModel } from "../model/session"; +import { createChatPrompt } from "../utils/chatPrompt"; +import { modelLight } from "../services/aiModels"; +import { userWantsChart } from "../utils/chatDetection"; +import { runChartAnalysis } from "../services/chatAiService"; + +import dataModel from "../model/dataModel"; + +function getUserId(req: Request): string | null { + return (req as any).user?.userId ?? null; +} + +export const createSession = async (req: Request, res: Response) => { + try { + const userId = getUserId(req); + + let session_token: string | null = null; + if (!userId) { + session_token = (req.headers["x-session-token"] as string) || crypto.randomUUID(); + } + + const payload: any = { + user_id: userId ? new mongoose.Types.ObjectId(userId) : null, + session_token: userId ? null : session_token, + data_id: req.body.data_id ? new mongoose.Types.ObjectId(req.body.data_id) : null, + title: req.body.title || "New Session", + messages: req.body.messages || [], + charts: req.body.charts || [], + metrics: req.body.metrics || {}, + }; + + const session = await SessionModel.create(payload); + + // Return consistent envelope the frontend expects + return res.status(201).json({ + session, + // only send session_token when we generated/used one (guest) + ...(session_token ? { session_token } : {}), + }); + } catch (err) { + console.error("createSession error", err); + return res.status(500).json({ error: "Server error" }); + } +}; + + +export const getSession = async (req: Request, res: Response) => { + try { + const userId = getUserId(req); + const id = req.params.id; + console.log(id, userId) + + if (!mongoose.Types.ObjectId.isValid(id)) + return res.status(400).json({ error: "Invalid session id" }); + + const session = await SessionModel.findOne({ + _id: id, + user_id: userId, + }) + .populate("data_id") + .lean(); + console.log(session); + + if (!session) return res.status(404).json({ error: "Not found" }); + + return res.json(session); + } catch (err) { + console.error("getSession error", err); + return res.status(500).json({ error: "Server error" }); + } +}; + +export const listSessions = async (req: Request, res: Response) => { + try { + const userId = getUserId(req); + + const sessions = await SessionModel.find({ user_id: userId }) + .sort({ updatedAt: -1 }) + .select("title createdAt updatedAt data_id") + .populate("data_id") + .lean(); + + return res.json({ sessions }); + } catch (err) { + console.error("listSessions error", err); + return res.status(500).json({ error: "Server error" }); + } +}; + +export const appendMessage = async (req: Request, res: Response) => { + try { + const userId = getUserId(req); + const id = req.params.id; + const userMessage = req.body.user; + + if (!mongoose.Types.ObjectId.isValid(id)) + return res.status(400).json({ error: "Invalid session id" }); + + const session = await SessionModel.findOne({ _id: id, user_id: userId }).populate("data_id"); + if (!session) return res.status(404).json({ error: "Session not found" }); + + const dataset = session.data_id; + if (!dataset) { + return res.json({ + reply: "Please upload a dataset first.", + chart: null, + }); + } + + let aiReply = ""; + let generatedChart = null; + + if (userWantsChart(userMessage)) { + generatedChart = await runChartAnalysis(userMessage, dataset); + + aiReply = generatedChart + ? "Here is the chart you requested." + : "I could not generate a chart for that query."; + } else { + const prompt = createChatPrompt(userMessage, dataset); + const chat = modelLight.startChat({ history: [] }); + const reply = await chat.sendMessage(prompt); + aiReply = reply.response.text(); + } + + console.log("chart",generatedChart); + + session.messages.push({ + user: userMessage, + ai: aiReply, + createdAt: new Date(), + }); + + if (generatedChart) { + console.log("inside generated chart") + session.charts.push(generatedChart.chart); + } + + session.updatedAt = new Date(); + await session.save(); + + return res.json({ + reply: aiReply, + chart: generatedChart, + }); + + } catch (err) { + console.error("appendMessage error", err); + return res.status(500).json({ error: "Server error" }); + } +}; + + + +export const appendChart = async (req: Request, res: Response) => { + try { + const userId = getUserId(req); + + if (!userId) { + return res.status(401).json({ error: "Unauthorized: No userId found" }); + } + + const id = req.params.id; + + if (!req.body.chart) + return res.status(400).json({ error: "Missing chart data" }); + + let session = await SessionModel.findById(id); + + if (!session) + return res.status(404).json({ error: "Session not found" }); + + if (!session.user_id) { + session.user_id = userId; + } + + if (session.user_id.toString() !== userId.toString()) { + return res.status(403).json({ error: "Unauthorized" }); + } + + // Now push chart + session.charts.push(req.body.chart); + session.updatedAt = new Date(); + await session.save(); + + return res.json(session); + + } catch (err) { + console.error("appendChart error", err); + return res.status(500).json({ error: "Server error" }); + } +}; + + + +export const updateSession = async (req: Request, res: Response) => { + try { + const userId = getUserId(req); + const id = req.params.id; + + const session = await SessionModel.findOneAndUpdate( + { _id: id, user_id: userId }, + { ...req.body, updatedAt: new Date() }, + { new: true } + ); + + return res.json(session); + } catch (err) { + console.error("updateSession error", err); + return res.status(500).json({ error: "Server error" }); + } +}; + +export const deleteSession = async (req: Request, res: Response) => { + try { + const userId = getUserId(req); + const id = req.params.id; + + await SessionModel.findOneAndDelete({ _id: id, user_id: userId }); + + return res.json({ ok: true }); + } catch (err) { + console.error("deleteSession error", err); + return res.status(500).json({ error: "Server error" }); + } +}; diff --git a/src/controllers/uploadController.ts b/src/controllers/uploadController.ts index ecdfdaa..b2a72b7 100644 --- a/src/controllers/uploadController.ts +++ b/src/controllers/uploadController.ts @@ -1,11 +1,130 @@ -import type { Request, Response } from "express" -export const fileupload = async (req: Request, res: Response) => { - try { - console.log("api reached here really") - res.send("hello"); +import type { Request, Response } from "express"; +import fs from "fs"; +import { CustomError } from "../utils/CustomError"; +import { parseCsvFile } from "../services/csvService"; +import { uploadCsvToR2 } from "../services/r2Service"; +import { createDatasetWithRows } from "../services/datasetService"; +import { analyzeDatasetWithAiOrFallback } from "../services/aiAnalysisService"; +import dataModel from "../model/dataModel"; +import mongoose from "mongoose"; +import { SessionModel } from "../model/session"; + + +interface UserPayload { + userId: mongoose.Types.ObjectId; + isGuest?: boolean; +} +type AuthedRequest = Request & { + user?: UserPayload; +}; + +export const fileParsing = async (req: Request, res: Response) => { + let filepath: string | undefined; + + try { + console.time("file-processing"); + + if (!req.file) { + return res.status(400).json({ message: "File Not Uploaded", success: false }); } - catch (err) { - console.log(err) - return res.status(500).json({ message: "internal server error", success: false }) + + const file = req.file as Express.Multer.File; + filepath = file.path; + + const { results, headers, totalRows } = await parseCsvFile(filepath); + console.log(`CSV parsed: ${totalRows} rows, ${headers.length} columns`); + console.timeEnd("file-processing"); + + const fileUrl = await uploadCsvToR2(filepath, file.originalname); + + // Remove temp file + fs.unlinkSync(filepath); + filepath = undefined; + + const totalColumns = headers.length; + let missingValues = 0; + for (const row of results) { + for (const val of Object.values(row)) { + if (val === "" || val === null || val === undefined) missingValues++; + } } -} \ No newline at end of file + + const computedMetrics = { + total_rows: totalRows, + total_columns: totalColumns, + missing_values: missingValues, + }; + const authedReq = req as AuthedRequest; + const userId = authedReq.user?.userId; + const dataset = await createDatasetWithRows( + results, + userId as mongoose.Types.ObjectId, + fileUrl, + file.originalname, + totalColumns + ); + + const aiResponse = await analyzeDatasetWithAiOrFallback(computedMetrics, dataset, results); + + if (!aiResponse.metrics || !aiResponse.charts || !Array.isArray(aiResponse.charts)) { + console.error(" Invalid AI response structure:", aiResponse); + throw new Error("AI response missing required fields"); + } + + console.log(" Final response summary:", { + totalRows: aiResponse.metrics.total_rows, + charts: aiResponse.charts.length, + barChartData: aiResponse.charts[0]?.data?.length || 0, + pieChartData: aiResponse.charts[1]?.data?.length || 0, + }); + //uploding datas to database + const session = await SessionModel.create({ + user_id: userId ?? null, + session_token: userId ? null : (req.headers["x-session-token"] as string) || null, + data_id: dataset._id, + title: req.body.title || file.originalname || "Uploaded dataset", + messages: [], + charts: aiResponse.charts || [], + metrics: aiResponse.metrics || {}, + }); + + const uploadSummary = async () => await dataModel.findByIdAndUpdate(dataset._id, { summary: aiResponse.summary }) + uploadSummary(); + + return res.status(200).json({ + success: true, + message: "Dataset processed successfully", + datasetId: dataset._id, + sessionId: session._id, + r2Url: fileUrl, + data: { + metrics: { + total_rows: aiResponse.metrics.total_rows, + total_columns: aiResponse.metrics.total_columns, + missing_values: aiResponse.metrics.missing_values, + charts_generated: aiResponse.charts.length, + }, + charts: aiResponse.charts, + summary: aiResponse.summary || [], + key_fields: aiResponse.key_fields || [], + }, + }); + } catch (err) { + console.error(" File parsing error:", err); + + if (filepath && fs.existsSync(filepath)) { + fs.unlinkSync(filepath); + } + + const error = new CustomError({ + errorData: err instanceof Error ? err.message : String(err), + statusCode: 500, + }); + + return res.status(error.statusCode).json({ + success: false, + message: "Dataset processing failed", + error: error.errorData, + }); + } +}; diff --git a/src/controllers/userController.ts b/src/controllers/userController.ts new file mode 100644 index 0000000..00b3184 --- /dev/null +++ b/src/controllers/userController.ts @@ -0,0 +1,101 @@ +import { Request, Response } from "express"; +import bcrypt from "bcrypt"; +import userModel from "../model/user"; +import { uploadimageToSupabase } from "../utils/imageUploader"; +import mongoose from "mongoose"; + +export const userImageUpdate = async (req: Request, res: Response) => { + try { + const { userId, image } = req.body; + + if (!userId || !image) { + return res.status(400).json({ message: "image and userId are required." }); + } + + const imageUrl = await uploadimageToSupabase(userId, image); + + let query: any; + if (mongoose.Types.ObjectId.isValid(userId)) { + query = { _id: userId }; + } else { + query = { googleId: userId }; + } + + const updatedUser = await userModel.findOneAndUpdate( + query, + { picture: imageUrl }, + { new: true, upsert: false }, + ); + + if (!updatedUser) { + return res.status(404).json({ message: "User not found" }); + } + + return res.status(200).json({ + message: "Profile image updated successfully", + imageUrl: updatedUser.picture, + user: updatedUser, + }); + } catch (err) { + console.error(" Error in userImageUpdate:", err); + res.status(500).json({ message: "Internal server error.", err }); + } +}; + +export const changePassword = async (req: Request, res: Response) => { + try { + const { userId, oldPassword, newPassword } = req.body; + + if (!userId || !oldPassword || !newPassword) { + return res.status(403).json({ message: "All fields required.." }); + } + const user = await userModel.findOne({ _id: userId }); + if (!user) { + return res.status(400).json({ message: "User is not found.." }); + } + + if (user.googleId) { + return res.status(403).json({ + message: "This account uses Google Login. Password cannot be changed.", + }); + } + const isCorrect = await bcrypt.compare(oldPassword, user.password!); + + if (!isCorrect) { + return res.status(400).json({ message: "Password is not matching.!" }); + } + + const hashed = await bcrypt.hash(newPassword, 10); + user.password = hashed; + await user.save(); + + return res.status(200).json({ message: "Password updated successfully" }); + } catch (err) { + res.status(500).json(err); + } +}; + +export const getUserProfile = async (req: Request, res: Response) => { + try { + const { userId } = req.params; + if (!userId) { + return res.status(400).json({ message: "User ID required" }); + } + + const user = await userModel.findOne({ + $or: [ + { _id: mongoose.Types.ObjectId.isValid(userId) ? userId : undefined }, + { googleId: userId }, + ].filter(Boolean), + }); + + if (!user) return res.status(404).json({ message: "User not found" }); + + return res.status(200).json({ + message: "User fetched successfully", + user, + }); + } catch (err) { + res.status(500).json({ message: "Error fetching user", err }); + } +}; diff --git a/src/middleware/verifytoken.ts b/src/middleware/verifytoken.ts deleted file mode 100644 index 5bc2c1d..0000000 --- a/src/middleware/verifytoken.ts +++ /dev/null @@ -1,19 +0,0 @@ -import Jwt from "jsonwebtoken"; -import { Request,Response,NextFunction } from "express"; - export const verifyToken = (req:Request,res:Response,next:NextFunction)=>{ - const authHeader = req.headers.authorization; - if(!authHeader){ - return res.status(401).json({message:"no token"}) - } - const token = authHeader.split(" ")[1] - try{ - const decoded = Jwt.verify(token,process.env.JWT_SECRET!); - req.user = decoded; - next(); - - }catch(err){ - return res.status(403).json({ message: "Invalid or expired token" }); - } - - - } \ No newline at end of file diff --git a/src/middlewares/cookieCheck.ts b/src/middlewares/cookieCheck.ts new file mode 100644 index 0000000..7809ab7 --- /dev/null +++ b/src/middlewares/cookieCheck.ts @@ -0,0 +1,16 @@ +import { NextFunction, Request, Response } from "express" + +export const cookieCheck = (req: Request, res: Response, next: NextFunction) => { + try { + res.clearCookie("userId", { + httpOnly: true, + secure: false, + sameSite: "strict", + }); + next(); + } + catch (err) { + console.log("failed to delete cookie :", err) + return res.status(500).json({ message: "internal server error:", success: false }) + } +} \ No newline at end of file diff --git a/src/middlewares/fileSizeCheck.ts b/src/middlewares/fileSizeCheck.ts new file mode 100644 index 0000000..1b74242 --- /dev/null +++ b/src/middlewares/fileSizeCheck.ts @@ -0,0 +1,16 @@ +import { NextFunction, Request, Response } from "express"; +import multer from "multer"; + +export const fileSizeCheck = (err: any, req: Request, res: Response, next: NextFunction) => { + if (err instanceof multer.MulterError) { + if (err.code === 'LIMIT_FILE_SIZE') { + return res.status(400).json({ + message: `File size too large. Max is ${process.env.MAX_FILE_SIZE} bytes.`, + }); + } + } + if (err) { + return res.status(400).json({ message: err.message }); + } + next(); +}; diff --git a/src/middleware/logincheck.ts b/src/middlewares/loginCheck.ts similarity index 100% rename from src/middleware/logincheck.ts rename to src/middlewares/loginCheck.ts diff --git a/src/middlewares/tokenCheck.ts b/src/middlewares/tokenCheck.ts new file mode 100644 index 0000000..0a25850 --- /dev/null +++ b/src/middlewares/tokenCheck.ts @@ -0,0 +1,109 @@ +import { NextFunction, Request, Response } from 'express'; +import Jwt from 'jsonwebtoken'; +import userModel from '../model/user'; +import guestModel from '../model/guest'; + +interface JwtPayload { + id: string; + userId: string; +} + +interface UserPayload { + userId: string; + isGuest?: boolean; +} + +type AuthedRequest = Request & { + user?: UserPayload; +}; + +export const tokenCheck = async (req: Request, res: Response, next: NextFunction) => { + try { + console.log('api now on tokencheck middleware'); + const authedReq = req as AuthedRequest; + const authHeader = req.headers.authorization; + try { + + if (authHeader?.startsWith('Bearer ')) { + const token = authHeader.split(' ')[1]; + + const decoded = Jwt.verify(token, process.env.JWT_SECRET as string) as JwtPayload; + authedReq.user = { + userId: decoded.id, + isGuest: false, + }; + console.log('Authenticated user from JWT:', decoded.id); + const currentUserToken = await userModel.findById({ _id: decoded.id }) + console.log("before cheking usertoken:") + if (currentUserToken?.token == 0) { + return res.json({ message: "Token is finished ! buy more token..", success: false }) + } + res.cookie('userId', decoded.id.toString(), { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'strict', + maxAge: 1 * 24 * 60 * 60 * 1000, + }); + return next(); + } + } + catch (err) { + console.log("error while cheking jwt:", err); + return res.status(401).json({message:"access token expired",success:false}) + } + const guestIdFromCookie = req.cookies?.userId; + + if (guestIdFromCookie) { + authedReq.user = { + userId: guestIdFromCookie, + isGuest: true, + }; + console.log('Using existing guest cookie:', guestIdFromCookie); + const UserToken = await guestModel.findById({ _id: req.cookies.userId }) + if (!UserToken) { + res.clearCookie("userId", { + httpOnly: true, + secure: false, + sameSite: "strict", + maxAge: 30 * 24 * 60 * 60 * 1000, + }); + return res.json({ message: "internal server error : Please upload the file again ", success: false }) + } + if (UserToken?.token == 0) { + return res.json({ message: "Token is finished ! buy more token..", success: false }) + } + return next(); + } + + console.log('Creating new guest user'); + + const newGuestUser = await guestModel.create({ + isGuest: true, + }); + + // setting the cookie + res.cookie('userId', newGuestUser._id.toString(), { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'strict', + maxAge: 1 * 24 * 60 * 60 * 1000, + }); + res.cookie('isGuest', true, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'strict', + maxAge: 1 * 24 * 60 * 60 * 1000, + }); + console.log('Guest cookie set successfully:', newGuestUser._id.toString()); + + authedReq.user = { + userId: newGuestUser._id.toString(), + isGuest: true, + }; + + return next(); + } catch (err) { + console.error('Error in tokenCheck middleware:', err); + return res.status(401).json({ message: 'Invalid or expired token' }); + } +}; diff --git a/src/middlewares/verifyToken.ts b/src/middlewares/verifyToken.ts new file mode 100644 index 0000000..274ca9b --- /dev/null +++ b/src/middlewares/verifyToken.ts @@ -0,0 +1,40 @@ +import Jwt from "jsonwebtoken"; +import { Request, Response, NextFunction } from "express"; +import refreshModel from "../model/refreshtoken"; + +export const verifyToken = async (req: Request, res: Response, next: NextFunction) => { + console.log("inside verify token"); + const authHeader = req.headers.authorization; + + if (!authHeader) { + return res.status(401).json({ message: "no token" }); + } + + const token = authHeader.split(" ")[1]; + console.log("TOKEN EXPIRY CHECK", Jwt.decode(token)); + + try { + const decoded = Jwt.verify(token, process.env.JWT_SECRET!); + req.user = decoded; + } catch (err) { + return res.status(401).json({ message: "Invalid or expired token" }); + } + + if (req.originalUrl.startsWith("/session")) { + const sessionId = req.headers["x-session-id"]; + console.log("session id:", sessionId); + + if (!sessionId || typeof sessionId !== "string") { + return res.status(401).json({ message: "Session ID missing" }); + } + + const session = await refreshModel.findOne({ _id: sessionId }); + if (!session) { + return res.status(401).json({ + message: "Session expired or logged out", + }); + } + } + + next(); +}; diff --git a/src/model/admin.ts b/src/model/admin.ts deleted file mode 100644 index 25bd05e..0000000 --- a/src/model/admin.ts +++ /dev/null @@ -1,21 +0,0 @@ -import mongoose, { Schema } from "mongoose"; - -interface Admin { - email: string, - password: string -}; - -const adminSchema = new Schema({ - - email: { - type: String, required: true - }, - password: { - type: String, required: true - }, - -}); - -const adminModel = mongoose.model('admin', adminSchema); - -export default adminModel; \ No newline at end of file diff --git a/src/model/admin/admin.ts b/src/model/admin/admin.ts new file mode 100644 index 0000000..9eafd0d --- /dev/null +++ b/src/model/admin/admin.ts @@ -0,0 +1,27 @@ +import { required } from 'joi'; +import mongoose, { Schema } from 'mongoose'; + +interface Admin { + email: string; + password: string; + role: string; +} + +const adminSchema = new Schema({ + email: { + type: String, + required: true, + }, + password: { + type: String, + required: true, + }, + role: { + type: String, + required: true, + }, +}); + +const adminModel = mongoose.model('admin', adminSchema); + +export default adminModel; diff --git a/src/model/admin/insights/deviceModel.ts b/src/model/admin/insights/deviceModel.ts new file mode 100644 index 0000000..c8412cf --- /dev/null +++ b/src/model/admin/insights/deviceModel.ts @@ -0,0 +1,22 @@ +import mongoose from "mongoose"; + +const deviceSchema = new mongoose.Schema({ + userId: { + type: String, + required: true, + }, + userAgent: { + type: String, + required: true, + }, + ipAddress: { + type: String, + required: true, + }, + createdAt: { + type: Date, + default: Date.now, + }, +}); + +export const deviceModel = mongoose.model("Device", deviceSchema); diff --git a/src/model/admin/insights/featureModel.ts b/src/model/admin/insights/featureModel.ts new file mode 100644 index 0000000..c87c088 --- /dev/null +++ b/src/model/admin/insights/featureModel.ts @@ -0,0 +1,10 @@ +import mongoose from "mongoose"; + +const featureUsageSchema = new mongoose.Schema({ + userId: { type: String, required: true }, + feature: { type: String, required: true }, + count: { type: Number, default: 1 }, + lastUsed: { type: Date, default: Date.now }, +}); + +export const FeatureUsageModel = mongoose.model("FeatureUsage", featureUsageSchema); diff --git a/src/model/chart.ts b/src/model/chart.ts index 5ed8318..3fe3db3 100644 --- a/src/model/chart.ts +++ b/src/model/chart.ts @@ -1,25 +1,49 @@ -import mongoose, { Schema } from "mongoose"; +// src/models/Chart.ts +import mongoose, { Schema, Document, Model } from "mongoose"; +import { IChartData, IChartPoint } from "../types/charts"; -interface Chart { - chat_id?: mongoose.Types.ObjectId, - user_id?: mongoose.Types.ObjectId, - chart_data?: any -}; +export interface IChart extends Document { + user_id: mongoose.Types.ObjectId; + session_id: mongoose.Types.ObjectId; + data_id?: mongoose.Types.ObjectId; + chart_data: IChartData[]; +} -const chartSchema = new Schema({ +const ChartPointSchema = new Schema( + { + xValue: String, + yValue: Number, + value: Number, + rawValue: Number, + formattedValue: String, + }, + { _id: false } +); - user_id: { - type: Schema.Types.ObjectId, ref: "user" - }, - chat_id: { - type: Schema.Types.ObjectId, ref: "chat" - }, - chart_data: { - type: String - }, +const ChartDataSchema = new Schema( + { + type: { type: String, enum: ["bar", "pie", "line"], required: true }, + title: { type: String, required: true }, + x: String, + y: String, + data: { type: [ChartPointSchema], default: [] }, + style: { type: Schema.Types.Mixed, default: {} }, + }, + { _id: false } +); -}); +const ChartSchema = new Schema( + { + user_id: { type: Schema.Types.ObjectId, ref: "user", required: true }, + session_id: { type: Schema.Types.ObjectId, ref: "sessions", required: true }, + data_id: { type: Schema.Types.ObjectId, ref: "datas" }, -const chartModel = mongoose.model('chart', chartSchema); + chart_data: { type: [ChartDataSchema], default: [] }, + }, + { timestamps: true } +); -export default chartModel; \ No newline at end of file +const ChartModel: Model = + mongoose.models.chart || mongoose.model("chart", ChartSchema); + +export default ChartModel; diff --git a/src/model/chat.ts b/src/model/chat.ts index dbb1caa..22642de 100644 --- a/src/model/chat.ts +++ b/src/model/chat.ts @@ -1,29 +1,37 @@ -import mongoose, { Schema } from "mongoose"; +import mongoose, { Schema, Document, Types } from "mongoose"; -interface Chat { - chart_id?: mongoose.Types.ObjectId, - user_id?: mongoose.Types.ObjectId, - data_id?: mongoose.Types.ObjectId, - chat: string -}; +export interface IChatMessage { + fromAi?: string | null; + fromUser?: string | null; + createdAt?: Date; +} -const chatSchema = new Schema({ +export interface IChat extends Document { + session_id?: Types.ObjectId; + messages: IChatMessage[]; + createdAt: Date; + updatedAt: Date; +} - user_id: { - type: Schema.Types.ObjectId, ref: "user" +const ChatSchema = new Schema( + { + session_id :{ + type: Schema.Types.ObjectId, + ref: "sessions", + required: true, }, - chart_id: { - type: Schema.Types.ObjectId, ref: "chart" - }, - data_id: { - type: Schema.Types.ObjectId, ref: "data" - }, - chat: { - type: String - } - -}); -const chatModel = mongoose.model('chat', chatSchema); + messages: [ + { + fromAi: { type: String, default: null }, + fromUser: { type: String, default: null }, + createdAt: { type: Date, default: () => new Date() }, + }, + ], + }, + { timestamps: true } +); -export default chatModel; \ No newline at end of file +export const ChatModel = + mongoose.models.Chat || mongoose.model("Chat", ChatSchema); +export default ChatModel \ No newline at end of file diff --git a/src/model/dataModel.ts b/src/model/dataModel.ts index 5eff803..9ea310c 100644 --- a/src/model/dataModel.ts +++ b/src/model/dataModel.ts @@ -1,35 +1,31 @@ -import mongoose, { Schema, SchemaType } from "mongoose"; -interface Data { - data: Record[]; - user_id: mongoose.Types.ObjectId, - chat_id?: string, - chart_id?: mongoose.Types.ObjectId, - createdAt: Date, - updatedAt: Date -}; +import mongoose, { Schema, model, Document } from "mongoose"; -const dataSchema = new Schema({ +export interface IDataset extends Document { + _id: mongoose.Types.ObjectId; + user_id: mongoose.Types.ObjectId; + name: string; + r2_url: string; + row_count: number; + column_count: number; + sample_data: any[]; + summary: string[]; + created_at: Date; +} - data: [ - { - type: Map, of: Schema.Types.Mixed - } - ], - user_id: { - type: Schema.Types.ObjectId, ref: "user" - }, - chat_id: { - type: Schema.Types.ObjectId, ref: "chat" - }, - chart_id: { - type: Schema.Types.ObjectId, ref: "chart" - } -}, - { - timestamps: true - }); +const DatasetSchema = new Schema({ + user_id: { type: mongoose.Types.ObjectId, default: null }, + name: { type: String, required: true }, + r2_url: { type: String, required: true }, + row_count: { type: Number, required: true }, + column_count: { type: Number, required: true }, + summary: { type: [String], required: true }, + sample_data: { + type: [Schema.Types.Mixed], + default: [], + }, -const dataModel = mongoose.model('data', dataSchema); + created_at: { type: Date, default: Date.now }, +}); -export default dataModel; \ No newline at end of file +export default model("datas", DatasetSchema); diff --git a/src/model/datasetRowModel.ts b/src/model/datasetRowModel.ts new file mode 100644 index 0000000..42761de --- /dev/null +++ b/src/model/datasetRowModel.ts @@ -0,0 +1,18 @@ +import { Schema, model, Document } from "mongoose"; + +export interface IDatasetRow extends Document { + datasetId: string; + [key: string]: any; +} + +const DatasetRowSchema = new Schema( + { + datasetId: { type: String, required: true, index: true }, + }, + { + strict: false, + minimize: true, + } +); + +export default model("DatasetRow", DatasetRowSchema, "dataset_rows"); diff --git a/src/model/guest.ts b/src/model/guest.ts index 57c5eef..6b8c23d 100644 --- a/src/model/guest.ts +++ b/src/model/guest.ts @@ -1,21 +1,20 @@ -import mongoose, { Schema } from "mongoose"; +import mongoose, { Schema } from 'mongoose'; interface Guest { - token: number, - IP_address: number -}; - -const guestSchema = new Schema({ + token: number; + IP_address: number; +} +const guestSchema = new Schema( + { token: { - type: Number + type: Number, + default: 3, }, - IP_address: { - type: Number - } - -}); + }, + { timestamps: true }, +); const guestModel = mongoose.model('guest', guestSchema); -export default guestModel; \ No newline at end of file +export default guestModel; diff --git a/src/model/refreshtoken.ts b/src/model/refreshtoken.ts new file mode 100644 index 0000000..ef238eb --- /dev/null +++ b/src/model/refreshtoken.ts @@ -0,0 +1,60 @@ +import mongoose from "mongoose"; + +export interface Irefreshtoken { + userId: mongoose.Types.ObjectId; + tokenhash: string; + expiresAt: Date; + userAgent?: string; + ip?: string; + createdAt: Date; + lastActiveAt: Date; + isValid: boolean; +} + +const refreshTokenSchema = new mongoose.Schema({ + userId: { + type: mongoose.Schema.Types.ObjectId, + ref: "user", + required: true, + }, + + tokenhash: { + type: String, + required: true, + }, + + expiresAt: { + type: Date, + required: true, + }, + + userAgent: { + type: String, + }, + + ip: { + type: String, + }, + + createdAt: { + type: Date, + default: Date.now, + }, + + lastActiveAt: { + type: Date, + default: Date.now, + }, + + isValid: { + type: Boolean, + default: true, + }, +}); + +const refreshModel = mongoose.model( + "refreshToken", + refreshTokenSchema +); + +export default refreshModel; diff --git a/src/model/session.ts b/src/model/session.ts new file mode 100644 index 0000000..eca2433 --- /dev/null +++ b/src/model/session.ts @@ -0,0 +1,62 @@ + import mongoose, { Schema, Document, Types } from "mongoose"; + + export interface ISessionMessage { + fromAi?: string | null; + fromUser?: string | null; + createdAt?: Date; + } + + export interface IChartData { + title: string; + type: "bar" | "pie" | "line"; + x?: string; + y?: string; + data: any[]; + style?: any; + } + + export interface ISession extends Document { + user_id: Types.ObjectId; + data_id?: Types.ObjectId; + title: string; + messages: ISessionMessage[]; + charts: IChartData[]; + metrics: any; + createdAt: Date; + updatedAt: Date; + } + + const SessionSchema = new Schema( + { + user_id: { type: Schema.Types.ObjectId, ref: "users_or_guest", required: true }, + + data_id: { type: Schema.Types.ObjectId, ref: "datas", default: null }, + + title: { type: String, default: "New Session" }, + + messages: [ + { + user: { type: String, default: "" }, + ai: { type: String, default: "" }, + createdAt: { type: Date, default: Date.now }, + }, + ], + + charts: [ + { + title: String, + type: { type: String, enum: ["bar", "pie", "line"] }, + x: String, + y: String, + data: Schema.Types.Mixed, + style: { type: Schema.Types.Mixed, default: {} }, + }, + ], + + metrics: { type: Schema.Types.Mixed, default: {} }, + }, + { timestamps: true } + ); + + export const SessionModel = + mongoose.models.sessions || mongoose.model("sessions", SessionSchema); diff --git a/src/model/user.ts b/src/model/user.ts index 0871b08..0dd8014 100644 --- a/src/model/user.ts +++ b/src/model/user.ts @@ -1,51 +1,71 @@ import mongoose, { Schema } from "mongoose"; enum Status { - Active = "active", - Disabled = "disabled" + Active = "active", + Disabled = "disabled" }; export interface User { - googleId?: number - name?: string, - picture?: string, - email: string, - password?: string, - token: number, - status?: Status, - isDeleted?: boolean + googleId?: number + name?: string, + picture?: string, + email: string, + password?: string, + token: number, + status?: Status, + isDeleted?: boolean, + place: String, + phone: Number }; -const userSchema = new Schema({ +const userSchema = new Schema( + { googleId: { - type: String, unique: true, sparse: true + type: String, + unique: true, + sparse: true }, name: { - type: String + type: String }, picture: { - type: String + type: String }, email: { - type: String, required: true + type: String, + required: true }, password: { - type: String, required: false + type: String, + required: false }, token: { - type: Number, - default: 3 + type: Number, + default: 3 + }, + place: { + type: String + }, + phone: { + type: Number }, status: { - type: String, - enum: Object.values(Status), - default: Status.Active, + type: String, + enum: Object.values(Status), + default: Status.Active }, isDeleted: { - type: Boolean, default: false + type: Boolean, + default: false }, + createdAt: { + type: Date, + default: Date.now + } + }, + { timestamps: true } +); -}); const userModel = mongoose.model("user", userSchema); diff --git a/src/routes/adminroutes/insightsRouter.ts b/src/routes/adminroutes/insightsRouter.ts new file mode 100644 index 0000000..1199ca9 --- /dev/null +++ b/src/routes/adminroutes/insightsRouter.ts @@ -0,0 +1,7 @@ +import express from "express"; +import { getUserDevices } from "../../admincontroller/insightsController"; +import { verifyToken } from "../../middlewares/verifyToken"; + +export const insightsRouter = express.Router() + +insightsRouter.get('/device' , getUserDevices) \ No newline at end of file diff --git a/src/routes/adminroutes/tokenRouter.ts b/src/routes/adminroutes/tokenRouter.ts new file mode 100644 index 0000000..1da0a3b --- /dev/null +++ b/src/routes/adminroutes/tokenRouter.ts @@ -0,0 +1,8 @@ +// import { Router } from 'express'; +// import { getAlltokencont } from '../../adminController/tokenController'; +// import { getAlltokenusage } from '../../adminController/tokenController'; +// import { verifyAdmin } from '../../middlewares/verifyAdmin'; +// export const tokenrouter = Router(); + +// tokenrouter.get('/alltokens', verifyAdmin, getAlltokencont); +// tokenrouter.get('/alltokenusage', getAlltokenusage); diff --git a/src/routes/adminroutes/userRouter.ts b/src/routes/adminroutes/userRouter.ts new file mode 100644 index 0000000..12d3366 --- /dev/null +++ b/src/routes/adminroutes/userRouter.ts @@ -0,0 +1,25 @@ +import { Router } from "express"; + +import { addGustuser } from "../../admincontroller/userController"; +import { loggedusers } from "../../admincontroller/userController"; +import { fetchAllgustusers } from "../../admincontroller/userController"; +import { getNewUsersPerMonth } from "../../admincontroller/userController"; +import { getAllusers } from "../../admincontroller/userController"; + +export const adminrouter = Router() + + +adminrouter.get("/loggedusers",loggedusers) +adminrouter.get("/gustusers",fetchAllgustusers) +adminrouter.get("/getallusers",getAllusers) +adminrouter.get("/newuserpermonth",getNewUsersPerMonth) + + + + + + + + +adminrouter.post("/addgustuser",addGustuser) + diff --git a/src/routes/authRoutes.ts b/src/routes/authRoutes.ts index 03491aa..e577058 100644 --- a/src/routes/authRoutes.ts +++ b/src/routes/authRoutes.ts @@ -1,10 +1,251 @@ import passport from "passport"; import { Router } from "express"; -import { googleCallback } from "../controller/auth/auth.ts"; -const googleRouter = Router(); +import { googleCallback } from "../auth/googleAuth.ts"; +import { register, loginCheck } from "../auth/auth.ts"; +import { verifyOtp, resendOtp } from "../auth/otp.ts"; +import { refreshAccessToken } from "../services/jwtServices.ts"; +import { deviceLogger } from "../utils/deviceLogger.ts"; +import { verifyToken } from "../middlewares/verifyToken.ts"; +import { forgotPassword } from "../auth/password.ts"; +import { verifyForgotOtp } from "../auth/otp.ts"; +import { resetPassword } from "../auth/password.ts"; +import { getAllSessions } from "../auth/auth.ts"; +import { logoutDevice } from "../auth/logoutAuth.ts"; +import { logoutAllDevices } from "../auth/logoutAuth.ts"; +import { cookieCheck } from "../middlewares/cookieCheck.ts"; -googleRouter.get('/google', passport.authenticate("google", { scope: ["Profile", "email"] })); -googleRouter.get('/google/callback', passport.authenticate("google", { session: false, failureRedirect: "http://localhost:5000/auth/google" }), googleCallback); +const authRouter = Router(); -export default googleRouter; \ No newline at end of file +authRouter.post("/logoutAllDevices", verifyToken, logoutAllDevices); + +/** + * @swagger + * /auth/login: + * post: + * summary: User login + * tags: [Auth] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - email + * - password + * properties: + * email: + * type: string + * password: + * type: string + * responses: + * 200: + * description: Login success + access token + * 400: + * description: Invalid credentials + */ +authRouter.post("/login", cookieCheck, loginCheck, deviceLogger); + +/** + * @swagger + * /auth/register: + * post: + * summary: Register a new user + * tags: [Auth] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - email + * - password + * properties: + * email: + * type: string + * password: + * type: string + * responses: + * 200: + * description: OTP sent + * 400: + * description: User already exists + */ +authRouter.post("/register", register); + +/** + * @swagger + * /auth/verifyOtp: + * post: + * summary: Verify user OTP during registration + * tags: [Auth] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - email + * - otp + * properties: + * email: + * type: string + * otp: + * type: string + * responses: + * 200: + * description: OTP verified successfully + * 400: + * description: Invalid OTP + */ +authRouter.post("/verifyOtp", verifyOtp); + +/** + * @swagger + * /auth/resendOtp: + * post: + * summary: Resend OTP + * tags: [Auth] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - email + * properties: + * email: + * type: string + * responses: + * 200: + * description: OTP resent + */ +authRouter.post("/resendOtp", resendOtp); + +/** + * @swagger + * /auth/newRefreshToken: + * post: + * summary: Get a new access token using refresh token + * tags: [Auth] + * responses: + * 200: + * description: New access token issued + * 401: + * description: Invalid or expired refresh token + */ +authRouter.post("/newRefreshToken", refreshAccessToken); + +/** + * @swagger + * /auth/forgotPassword: + * post: + * summary: Request OTP for forgotten password + * tags: [Auth] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - email + * properties: + * email: + * type: string + * responses: + * 200: + * description: OTP sent + */ +authRouter.post("/forgotPassword", forgotPassword); + +/** + * @swagger + * /auth/verifyForgotOtp: + * post: + * summary: Verify OTP for password reset + * tags: [Auth] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - email + * - otp + * properties: + * email: + * type: string + * otp: + * type: string + */ +authRouter.post("/verifyForgotOtp", verifyForgotOtp); + +/** + * @swagger + * /auth/resetPassword: + * post: + * summary: Reset user password + * tags: [Auth] + */ +authRouter.post("/resetPassword", resetPassword); + +/** + * @swagger + * /auth/getAllSessions: + * get: + * summary: Get all user login sessions + * tags: [Auth] + * security: + * - bearerAuth: [] + */ +authRouter.get("/getAllSessions", verifyToken, getAllSessions); + +/** + * @swagger + * /auth/logoutDevice: + * post: + * summary: Logout from a single device session + * tags: [Auth] + * security: + * - bearerAuth: [] + */ +authRouter.post("/logoutDevice", verifyToken, logoutDevice); + +// google authentication +/** + * @swagger + * /auth/google: + * get: + * summary: Google OAuth login + * tags: [Auth] + */ +authRouter.get("/google", cookieCheck, (req, res, next) => { + const redirect = typeof req.query.redirect === "string" ? req.query.redirect : "/home"; + const authenticator = passport.authenticate("google", { + scope: ["profile", "email"], + state: redirect, + }); + + authenticator(req, res, next); +}); + +/** + * @swagger + * /auth/google/callback: + * get: + * summary: Google OAuth callback + * tags: [Auth] + */ +authRouter.get( + "/google/callback", + passport.authenticate("google", { session: false, failureRedirect: "/auth/google" }), + googleCallback, +); + +export default authRouter; diff --git a/src/routes/chatRouter.ts b/src/routes/chatRouter.ts new file mode 100644 index 0000000..96dad28 --- /dev/null +++ b/src/routes/chatRouter.ts @@ -0,0 +1,8 @@ +import express from "express"; +import {chatController }from "../controllers/chatController" + +const chatRouter = express.Router(); + +chatRouter.post('/',chatController); + +export default chatRouter \ No newline at end of file diff --git a/src/routes/paymentRoutes.ts b/src/routes/paymentRoutes.ts index 68f8d58..a643445 100644 --- a/src/routes/paymentRoutes.ts +++ b/src/routes/paymentRoutes.ts @@ -1,12 +1,68 @@ import express from "express"; -import { createCheckoutSession, handleWebhook } from "../controllers/paymentController.js"; +import { + createCheckoutSession, + handleWebhook, +} from "../controllers/paymentController.ts"; const router = express.Router(); -// Create Checkout Session +/** + * @swagger + * tags: + * name: Payment + * description: Payment and Stripe operations + */ + +/** + * @swagger + * /payment/create-checkout-session: + * post: + * summary: Create Stripe checkout session + * tags: [Payment] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * example: + * planId: "basic" + * price: 299 + * responses: + * 200: + * description: Checkout session created + * 400: + * description: Invalid request + * 500: + * description: Server error + */ router.post("/create-checkout-session", createCheckoutSession); -// Stripe Webhook -router.post("/webhook", express.raw({ type: "application/json" }), handleWebhook); +/** + * @swagger + * /payment/webhook: + * post: + * summary: Stripe webhook endpoint (RAW body) + * description: Stripe uses this to notify your server about events. **No auth is required.** + * tags: [Payment] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * responses: + * 200: + * description: Webhook received successfully + * 400: + * description: Invalid signature or error + */ +router.post( + "/webhook", + express.raw({ type: "application/json" }), + handleWebhook +); -export default router; +export default router; diff --git a/src/routes/sessionRoutes.ts b/src/routes/sessionRoutes.ts new file mode 100644 index 0000000..06d8fa9 --- /dev/null +++ b/src/routes/sessionRoutes.ts @@ -0,0 +1,18 @@ +// src/routes/sessionRoutes.ts +import express from "express"; +import * as controller from "../controllers/sessionController"; +import {tokenCheck} from "../middlewares/tokenCheck"; + +const sessionRouter = express.Router(); + +sessionRouter.use(tokenCheck); + +sessionRouter.post("/", controller.createSession); +sessionRouter.get("/", controller.listSessions); +sessionRouter.get("/:id", controller.getSession); +sessionRouter.patch("/:id", controller.updateSession); +sessionRouter.post("/:id/message", controller.appendMessage); +sessionRouter.post("/:id/chart", controller.appendChart); +sessionRouter.delete("/:id", controller.deleteSession); + +export default sessionRouter; diff --git a/src/routes/uploadRouter.ts b/src/routes/uploadRouter.ts index b652d76..9e4cdc2 100644 --- a/src/routes/uploadRouter.ts +++ b/src/routes/uploadRouter.ts @@ -1,7 +1,47 @@ -import { Router } from 'express' -import { fileupload } from '../controllers/uploadController.js' +import { Router } from "express"; +import upload from "../utils/multerUpload"; +import { fileParsing } from "../controllers/uploadController"; +import { tokenCheck } from "../middlewares/tokenCheck"; -const uploadRouter = Router() -uploadRouter.get("/", fileupload); +const uploadRouter = Router(); -export default uploadRouter; \ No newline at end of file +/** + * @swagger + * tags: + * name: Upload + * description: File upload and parsing + */ + +/** + * @swagger + * /upload/fileupload: + * post: + * summary: Upload a file and parse it + * description: Uploads a file using Multer and parses its content. + * tags: [Upload] + * security: + * - bearerAuth: [] # JWT required + * requestBody: + * required: true + * content: + * multipart/form-data: + * schema: + * type: object + * properties: + * file: + * type: string + * format: binary + * description: File to upload + * responses: + * 200: + * description: File uploaded and parsed successfully + * 400: + * description: Invalid or missing file + * 401: + * description: Unauthorized — JWT missing or invalid + * 500: + * description: Server error + */ +uploadRouter.post("/fileupload",tokenCheck, upload.single("file"), fileParsing); + +export default uploadRouter; diff --git a/src/routes/userRouter.ts b/src/routes/userRouter.ts index bb1e3ed..7f10bda 100644 --- a/src/routes/userRouter.ts +++ b/src/routes/userRouter.ts @@ -1,18 +1,110 @@ import { Router } from "express"; -import { register,loginCheck,verifyOtp,getAllUser } from "../controller/auth/auth.ts"; -import { verifyToken } from "../middleware/verifytoken.ts"; -import { testpro } from "../controller/auth/auth.ts"; +import { getUserProfile } from "../controllers/userController.ts"; +import { changePassword, userImageUpdate } from "../controllers/userController.ts"; +import { verifyToken } from "../middlewares/verifyToken.ts"; const userRouter = Router(); -userRouter.post('/login',loginCheck); -userRouter.post("/register",register); -userRouter.post("/verifyOtp",verifyOtp); -userRouter.get("/test",verifyToken,testpro) -userRouter.post("/register", register); -userRouter.post("/verifyOtp", verifyOtp); +/** + * @swagger + * tags: + * name: User + * description: User management and profile operations + */ -// dummy route dont take it serious -userRouter.get('/alluser', getAllUser) +/** + * @swagger + * /user/{userId}: + * get: + * summary: Get user profile + * description: Fetches the profile details of a user using userId. + * tags: [User] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: userId + * required: true + * schema: + * type: string + * description: ID of the user to fetch + * responses: + * 200: + * description: User profile fetched successfully + * 401: + * description: Unauthorized - Token missing or invalid + * 404: + * description: User not found + */ +userRouter.get("/:userId", verifyToken, getUserProfile); -export default userRouter; \ No newline at end of file +/** + * @swagger + * /user/upload: + * put: + * summary: Update user profile image + * description: Allows a user to upload or update their profile picture. + * tags: [User] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * multipart/form-data: + * schema: + * type: object + * properties: + * image: + * type: string + * format: binary + * description: Image file to upload + * responses: + * 200: + * description: Profile image updated successfully + * 400: + * description: Invalid image + * 500: + * description: Server error + */ +userRouter.put("/upload", verifyToken, userImageUpdate); + +/** + * @swagger + * /user/newpassword: + * post: + * summary: Change user password + * description: Allows a user to change their password. + * tags: [User] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - userId + * - oldPassword + * - newPassword + * properties: + * userId: + * type: string + * format: objectId + * oldPassword: + * type: string + * newPassword: + * type: string + * responses: + * 200: + * description: Password changed successfully + * 400: + * description: Incorrect old password + * 401: + * description: Unauthorized + * 500: + * description: Server error + */ +userRouter.post("/newpassword", verifyToken, changePassword); + +export default userRouter; diff --git a/src/services/aiAnalysisService.ts b/src/services/aiAnalysisService.ts new file mode 100644 index 0000000..99400d1 --- /dev/null +++ b/src/services/aiAnalysisService.ts @@ -0,0 +1,185 @@ +// services/aiAnalysisService.ts + +import { generateAiPrompt } from "../utils/aiPrompt"; +import mcpClient from "../services/mcpClient"; +import { generateChartsFromData } from "./chartGenerator"; + +// import your central model + API switching +import { getModel, switchApi, getApiKeyCount } from "./switchingApi"; + +export async function analyzeDatasetWithAiOrFallback( + computedMetrics: { total_rows: number; total_columns: number; missing_values: number }, + dataset: any, + results: any[] +) { + let aiResponse = null; + + const tools = await mcpClient.listTools(); + const prompt = generateAiPrompt(computedMetrics, dataset, tools); + + let attempts = 0; + + while (attempts < getApiKeyCount() && !aiResponse) { + try { + const model = getModel(); + const chat = model.startChat({ history: [] }); + + let result = await chat.sendMessage(prompt); + let response = result.response; + + let iteration = 0; + + while (iteration < 10) { + const fn = response.functionCalls()?.[0]; + if (!fn) break; + + const toolResult = await mcpClient.call(fn.name, fn.args); + + const next = await chat.sendMessage([ + { + functionResponse: { + name: fn.name, + response: toolResult, + }, + }, + ]); + + response = next.response; + iteration++; + } + + const text = response.text().trim().replace(/```json|```/g, ""); + aiResponse = JSON.parse(text); + + console.log("AI analysis completed with key", attempts + 1); + } catch (err: any) { + const msg = String(err?.message || ""); + console.log("Gemini error:", msg); + + if ( + msg.includes("quota") || + msg.includes("429") || + msg.includes("exceeded") || + err.status === 503 + ) { + console.log("Rate limit switching key..."); + switchApi(); + attempts++; + continue; + } + + console.log("Non-limit AI error stopping."); + break; + } + } + if (!aiResponse) { + console.log("Fallback → local chart + intelligent insight generation"); + + const { barData, pieData, columns } = generateChartsFromData(results); + + // Basic metrics + const totalRows = computedMetrics.total_rows; + const totalCols = computedMetrics.total_columns; + const missing = computedMetrics.missing_values; + + // Chart info + const topBar = barData[0]; + const topPie = pieData[0]; + + // Detect imbalance + const pieTotal = pieData.reduce((s, p) => s + p.value, 0); + const pieDominance = + topPie && pieTotal > 0 ? (topPie.value / pieTotal) * 100 : 0; + + // Detect if numeric column is skewed / high variance + const numericCols = results.length + ? Object.keys(results[0]).filter((c) => + results.some((r) => !isNaN(Number(r[c]))) + ) + : []; + + let skewNotes : any = []; + numericCols.forEach((col) => { + const nums = results + .map((r) => Number(r[col])) + .filter((n) => !isNaN(n)); + if (nums.length < 5) return; + + const mean = nums.reduce((a, b) => a + b, 0) / nums.length; + const max = Math.max(...nums); + const min = Math.min(...nums); + + if (max > mean * 4) { + skewNotes.push(`${col} has very large outliers.`); + } + if (min < mean * 0.25) { + skewNotes.push(`${col} is highly left-skewed.`); + } + }); + + // Build human-quality summary + const summary = [ + `Your dataset contains ${totalRows} rows and ${totalCols} columns, giving enough data for reliable analysis.`, + + missing > 0 + ? `There are ${missing} missing values, indicating that some cleaning or preprocessing may improve accuracy.` + : `No missing values detected — the dataset appears clean and consistent.`, + + topBar + ? `In the bar chart analysis, ${topBar.xValue} contributes the most with a total of ${topBar.yValue}, making it the most influential category for numeric trends.` + : `Not enough numeric/categorical combinations found to generate a bar insight.`, + + topPie + ? `In the category distribution, ${topPie.xValue} is the dominant group with ${topPie.value} occurrences, representing ${pieDominance.toFixed( + 1 + )}% of all records.` + : `Pie distribution could not identify strong category groupings.`, + + pieDominance > 60 + ? `The dataset has high category imbalance, meaning one category dominates the distribution.` + : pieDominance > 30 + ? `Category distribution shows moderate imbalance, with a few categories standing out.` + : `Category distribution appears balanced without extreme dominance.`, + + skewNotes.length > 0 + ? `Notable numeric anomalies: ${skewNotes.join(" ")}` + : `Numeric columns appear evenly distributed without strong outliers.`, + + `Key fields that provide the most insight: ${Object.keys(results[0]) + .slice(0, 5) + .join(", ")}.`, + ]; + + aiResponse = { + metrics: computedMetrics, + + charts: [ + { + type: "bar", + title: `${columns.barChartNumeric} by ${columns.barChartCategory}`, + description: `Visualizes how ${columns.barChartNumeric} values change across different ${columns.barChartCategory} groups.`, + x: columns.barChartCategory, + y: columns.barChartNumeric, + data: barData, + style: { layout: "vertical", limit: 15 }, + }, + { + type: "pie", + title: `Distribution of ${columns.pieChartCategory}`, + description: `Shows the proportion of records by ${columns.pieChartCategory}, helping highlight dominant categories.`, + x: columns.pieChartCategory, + y: "count", + data: pieData, + style: { showLabels: true, showLegend: true }, + }, + ], + + summary: summary.filter(Boolean), // remove empty items + + key_fields: Object.keys(results[0] || {}).slice(0, 5), + }; +} + + + return aiResponse; +} diff --git a/src/services/aiModels.ts b/src/services/aiModels.ts new file mode 100644 index 0000000..cf33f82 --- /dev/null +++ b/src/services/aiModels.ts @@ -0,0 +1,38 @@ +// services/aiModels.ts +import { GoogleGenerativeAI } from "@google/generative-ai"; +import { mcpToolDeclarations } from "../config/mcpTools"; + +// === HELPER: get first non-empty key from comma-separated env === +function getFirstKey(envVar?: string): string { + if (!envVar) { + throw new Error("Missing Gemini API key environment variable"); + } + + const keys = envVar + .split(",") + .map(k => k.trim()) + .filter(Boolean); + + if (keys.length === 0) { + throw new Error("Gemini API key env is defined but no valid keys found"); + } + + return keys[0]; // use first key by default +} + +// ========== HEAVY MODEL (for charts with MCP tools) ========== +const heavyApiKey = getFirstKey(process.env.GEMINI_API_KEY); +const genAI_heavy = new GoogleGenerativeAI(heavyApiKey); + +export const modelHeavy = genAI_heavy.getGenerativeModel({ + model: "gemini-2.5-flash", + tools: [{ functionDeclarations: mcpToolDeclarations }], +}); + +// ========== LIGHT MODEL (for normal chat) ========== +const lightApiKey = getFirstKey(process.env.GEMINI_API_LIGHT_KEY); +const genAI_light = new GoogleGenerativeAI(lightApiKey); + +export const modelLight = genAI_light.getGenerativeModel({ + model: "gemini-2.0-flash-lite-preview", +}); diff --git a/src/services/aiPersistence.ts b/src/services/aiPersistence.ts new file mode 100644 index 0000000..22b7a91 --- /dev/null +++ b/src/services/aiPersistence.ts @@ -0,0 +1,29 @@ +import { SessionModel } from "../model/session"; + +export async function saveAiResponseIntoSession(userId: string, sessionId: string | null, aiPayload: any) { + if (!aiPayload) throw new Error("No aiPayload"); + + if (sessionId) { + return SessionModel.findByIdAndUpdate( + sessionId, + { + $push: { + messages: { $each: aiPayload.messages || [] }, + charts: { $each: aiPayload.charts || [] }, + }, + $set: { metrics: aiPayload.metrics || {}, updatedAt: new Date() }, + }, + { new: true } + ); + } else { + const s = await SessionModel.create({ + user_id: userId, + title: aiPayload.title || "AI analysis", + messages: aiPayload.messages || [], + charts: aiPayload.charts || [], + metrics: aiPayload.metrics || {}, + }); + + return s; + } +} diff --git a/src/services/chartGenerator.ts b/src/services/chartGenerator.ts new file mode 100644 index 0000000..fb107cd --- /dev/null +++ b/src/services/chartGenerator.ts @@ -0,0 +1,210 @@ +import { GeneratedCharts, ColumnAnalysis } from '../types/charts'; + +export function generateChartsFromData(results: any[]): GeneratedCharts { + console.log('Generating charts with intelligent column selection...'); + + if (results.length === 0) { + console.log(' No data to generate charts from'); + return { + barData: [], + pieData: [], + columns: { barChartCategory: '', barChartNumeric: '', pieChartCategory: '' } + }; + } + + const sampleRow = results[0]; + const columns = Object.keys(sampleRow); + + const columnAnalysis: ColumnAnalysis[] = columns.map(col => { + const values = results.map(row => row[col]); + const nonNullValues = values.filter(v => v !== '' && v !== null && v !== undefined); + const uniqueValues = new Set(nonNullValues); + + const numericValues = nonNullValues + .filter(v => !isNaN(Number(v))) + .map(Number); + const isNumeric = numericValues.length > nonNullValues.length * 0.7; + + const datePatterns = /\d{4}-\d{2}-\d{2}|\d{2}\/\d{2}\/\d{4}|\d{1,2}-\w{3}-\d{4}/; + const hasDatePattern = nonNullValues.some(v => datePatterns.test(String(v))); + + let variance = 0; + if (isNumeric && numericValues.length > 0) { + const mean = numericValues.reduce((a, b) => a + b, 0) / numericValues.length; + variance = numericValues.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / numericValues.length; + } + + const avgLength = + nonNullValues.reduce((sum, v) => sum + String(v).length, 0) / nonNullValues.length; + + let type: ColumnAnalysis['type']; + if (hasDatePattern) { + type = 'date'; + } else if (isNumeric) { + type = 'numeric'; + } else if (uniqueValues.size < nonNullValues.length * 0.5 && avgLength < 50) { + type = 'categorical'; + } else { + type = 'text'; + } + + return { + name: col, + type, + uniqueCount: uniqueValues.size, + nullCount: values.length - nonNullValues.length, + sampleValues: Array.from(uniqueValues).slice(0, 5), + variance, + avgLength + }; + }); + + console.log(' Column analysis:', columnAnalysis.map(c => ({ + name: c.name, + type: c.type, + unique: c.uniqueCount + }))); + + const scoreColumn = (col: ColumnAnalysis, purpose: 'category' | 'numeric' | 'pie-category') => { + let score = 0; + + if (purpose === 'category') { + if (col.type === 'categorical') score += 100; + if (col.uniqueCount >= 3 && col.uniqueCount <= 50) score += 50; + if (col.uniqueCount > 50 && col.uniqueCount <= 100) score += 20; + if (col.avgLength && col.avgLength < 30) score += 30; + if (col.nullCount === 0) score += 20; + + if (col.uniqueCount > 100) score -= 50; + if (col.avgLength && col.avgLength > 50) score -= 30; + if (col.type === 'text') score -= 40; + + const categoryKeywords = [ + 'category','type','status','group','name','class', + 'region','country','city','department','product','brand' + ]; + if (categoryKeywords.some(kw => col.name.toLowerCase().includes(kw))) score += 40; + + } else if (purpose === 'numeric') { + if (col.type === 'numeric') score += 100; + if (col.variance && col.variance > 0) score += 50; + if (col.nullCount === 0) score += 20; + + const numericKeywords = [ + 'amount','price','cost','value','total','sum','count', + 'quantity','revenue','sales','score','rating' + ]; + if (numericKeywords.some(kw => col.name.toLowerCase().includes(kw))) score += 40; + + if (col.name.toLowerCase().includes('id')) score -= 60; + if (col.uniqueCount === results.length) score -= 40; + } else { + if (col.type === 'categorical') score += 100; + if (col.uniqueCount >= 3 && col.uniqueCount <= 12) score += 60; + if (col.uniqueCount > 12 && col.uniqueCount <= 30) score += 30; + if (col.avgLength && col.avgLength < 25) score += 30; + if (col.nullCount === 0) score += 20; + + if (col.uniqueCount > 30) score -= 40; + if (col.uniqueCount < 3) score -= 40; + + const statusKeywords = ['status','state','type','category','priority','level']; + if (statusKeywords.some(kw => col.name.toLowerCase().includes(kw))) score += 50; + } + + return Math.max(0, score); + }; + + const categoricalCols = columnAnalysis.filter(c => c.type === 'categorical' || c.type === 'date'); + const numericCols = columnAnalysis.filter(c => c.type === 'numeric'); + + const barCategoryScores = categoricalCols + .map(c => ({ col: c, score: scoreColumn(c, 'category') })) + .sort((a, b) => b.score - a.score); + + const barNumericScores = numericCols + .map(c => ({ col: c, score: scoreColumn(c, 'numeric') })) + .sort((a, b) => b.score - a.score); + + const barChartCategory = barCategoryScores[0]?.col.name || columns[0]; + const barChartNumeric = barNumericScores[0]?.col.name || columns[1]; + + const pieCategoryScores = categoricalCols + .filter(c => c.name !== barChartCategory) + .map(c => ({ col: c, score: scoreColumn(c, 'pie-category') })) + .sort((a, b) => b.score - a.score); + + const pieChartCategory = + pieCategoryScores[0]?.col.name || + categoricalCols.find(c => c.name !== barChartCategory)?.name || + barChartCategory; + + console.log('Selected columns:', { + barChart: { + category: barChartCategory, + numeric: barChartNumeric, + categoryScore: barCategoryScores[0]?.score, + numericScore: barNumericScores[0]?.score + }, + pieChart: { + category: pieChartCategory, + score: pieCategoryScores[0]?.score + } + }); + + const barChartMap = new Map(); + let barSkippedRows = 0; + + results.forEach(row => { + const category = String(row[barChartCategory] || '').trim(); + const value = parseFloat(row[barChartNumeric]); + + if (category && !isNaN(value)) { + const current = barChartMap.get(category) || 0; + barChartMap.set(category, current + value); + } else { + barSkippedRows++; + } + }); + + const barData = Array.from(barChartMap.entries()) + .map(([xValue, yValue]) => ({ + xValue, + yValue: Math.round(yValue * 100) / 100 + })) + .sort((a, b) => b.yValue - a.yValue) + .slice(0, 15); + + console.log(`Bar chart: ${barData.length} categories (skipped ${barSkippedRows} rows)`); + + const pieChartMap = new Map(); + let pieSkippedRows = 0; + + results.forEach(row => { + const category = String(row[pieChartCategory] || '').trim(); + if (category && !['unknown','null'].includes(category.toLowerCase())) { + const current = pieChartMap.get(category) || 0; + pieChartMap.set(category, current + 1); + } else { + pieSkippedRows++; + } + }); + + const pieEntries = Array.from(pieChartMap.entries()) + .sort((a, b) => b[1] - a[1]); + + let pieData = pieEntries.slice(0, 8).map(([xValue, value]) => ({ xValue, value })); + + if (pieEntries.length > 8) { + const otherSum = pieEntries.slice(8).reduce((sum, [_, value]) => sum + value, 0); + pieData.push({ xValue: 'Other', value: otherSum }); + } + + console.log(` Pie chart: ${pieData.length} categories (skipped ${pieSkippedRows} rows)`); + + return { + barData, + pieData, + columns: { barChartCategory, barChartNumeric, pieChartCategory } + }; +} diff --git a/src/services/chatAiService.ts b/src/services/chatAiService.ts new file mode 100644 index 0000000..1e9de40 --- /dev/null +++ b/src/services/chatAiService.ts @@ -0,0 +1,61 @@ +import { modelHeavy } from "./aiModels"; +import { generateChartPrompt } from "../utils/chartPrompt"; +import mcpClient from "../services/mcpClient"; + +export async function runChartAnalysis(message: string, dataset: any) { + const prompt = generateChartPrompt(message, dataset); + + const chat = modelHeavy.startChat({ history: [] }); + let result = await chat.sendMessage(prompt); + let response = result.response; + + // handle tool calls + let iteration = 0; + const MAX_ITER = 10; + + while (iteration < MAX_ITER) { + const toolCall = response.functionCalls()?.[0]; + if (!toolCall) break; + + const toolResult = await mcpClient.call(toolCall.name, toolCall.args); + + const next = await chat.sendMessage([ + { + functionResponse: { + name: toolCall.name, + response: toolResult, + }, + }, + ]); + + response = next.response; + iteration++; + } + + // SAFE JSON PARSING + const rawText = response.text().trim(); + + // remove ```json and ``` fences + const withoutFences = rawText.replace(/```json\s*|```\s*/g, "").trim(); + + // in case the model adds text around the JSON, grab only the outermost object + const start = withoutFences.indexOf("{"); + const end = withoutFences.lastIndexOf("}"); + + if (start === -1 || end === -1) { + console.error("Model did not return a JSON object:", rawText); + throw new Error("AI did not return valid JSON"); + } + + const jsonSlice = withoutFences.substring(start, end + 1); + + let parsed; + try { + parsed = JSON.parse(jsonSlice); + } catch (e) { + console.error("Failed to parse AI JSON in runChartAnalysis:", e, "\nTEXT:", rawText); + throw e; + } + + return parsed; +} \ No newline at end of file diff --git a/src/services/csvService.ts b/src/services/csvService.ts new file mode 100644 index 0000000..635a7de --- /dev/null +++ b/src/services/csvService.ts @@ -0,0 +1,34 @@ +import fs from 'fs'; +import csv from 'csv-parser'; + +export interface ParsedCsv { + results: any[]; + headers: string[]; + totalRows: number; +} + +export function parseCsvFile(filepath: string): Promise { + return new Promise((resolve, reject) => { + const results: any[] = []; + let headers: string[] = []; + let totalRows = 0; + + const stream = fs.createReadStream(filepath, { encoding: 'utf-8' }).pipe(csv()); + + stream + .on('headers', (hdrs: string[]) => { + headers = hdrs; + }) + .on('data', (data: any) => { + totalRows++; + results.push(data); + }) + .on('error', (err: any) => { + console.error('CSV parsing error:', err); + reject(err); + }) + .on('end', () => { + resolve({ results, headers, totalRows }); + }); + }); +} diff --git a/src/services/datasetService.ts b/src/services/datasetService.ts new file mode 100644 index 0000000..6c0b901 --- /dev/null +++ b/src/services/datasetService.ts @@ -0,0 +1,41 @@ +import mongoose from "mongoose"; +import Dataset, { IDataset } from "../model/dataModel"; +import DatasetRow from "../model/datasetRowModel"; + +export async function createDatasetWithRows( + results: any[], + userId: mongoose.Types.ObjectId | null, + fileUrl: string, + fileName: string, + totalColumns: number +) { + console.time("dataset uploading to mongodb:") + // 1) Create dataset metadata doc (with sample rows) + const dataset = await Dataset.create({ + user_id: userId, + name: fileName, + r2_url: fileUrl, + row_count: results.length, + column_count: totalColumns, + sample_data: results.slice(0, 10), + }) as IDataset; + + console.log("Dataset saved to MongoDB:", dataset._id); + console.timeEnd("dataset uploading to mongodb:") + + // 2) Insert all rows into dataset_rows + const datasetId = dataset._id.toString(); + console.time("rows uploading in mongodb:") + const rowsToInsert = results.map((row) => ({ + datasetId, + ...row, + })); + + if (rowsToInsert.length > 0) { + await DatasetRow.insertMany(rowsToInsert, { ordered: false }); + console.log(`Inserted ${rowsToInsert.length} rows into dataset_rows`); + } + console.timeEnd("rows uploading in mongodb:") + + return dataset; +} diff --git a/src/services/jwtServices.ts b/src/services/jwtServices.ts index e2732b7..82643bc 100644 --- a/src/services/jwtServices.ts +++ b/src/services/jwtServices.ts @@ -1,6 +1,37 @@ import jwt from "jsonwebtoken"; +import { Response, Request } from "express"; +import { hashToken } from "../utils/hashTokens"; +import refreshModel from "../model/refreshtoken"; export const signJwt = (payload: object) => { - return jwt.sign(payload, process.env.JWT_SECRET!, { expiresIn: "7d" }); + return jwt.sign(payload, process.env.JWT_SECRET!, { expiresIn: "10s" }); }; +export const refreshAccessToken = async (req: Request, res: Response) => { + const refreshToken = req.cookies.refreshToken; + if (!refreshToken) { + return res.status(401).json({ success: false, message: "no refresh token" }); + } + try { + const correctCheck = jwt.verify(refreshToken, process.env.REFRESH_SECRET!) as { + id: string; + email: string; + }; + const hashedToken = hashToken(refreshToken); + const savedToken = await refreshModel.findOne({ tokenhash: hashedToken }); + if (!savedToken) { + return res.status(401).json({ success: false, message: "invalid refreshtoken" }); + } + + const jwtPayload = { id: correctCheck.id, email: correctCheck.email }; + + const newAccessToken = jwt.sign(jwtPayload, process.env.JWT_SECRET!, { + expiresIn: "15m", + }); + + return res.status(200).json({ success: true, newAccessToken }); + } catch (err) { + console.log("error in refresh token worked", err); + return res.status(500).json({ success: false, message: "internal server error" }); + } +}; diff --git a/src/services/mcpClient.ts b/src/services/mcpClient.ts new file mode 100644 index 0000000..42a13aa --- /dev/null +++ b/src/services/mcpClient.ts @@ -0,0 +1,133 @@ +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; + +class MCPClient { + private static instance: MCPClient; + private client: Client | null = null; + private connecting: Promise | null = null; + + private constructor() { } + + static getInstance() { + if (!MCPClient.instance) { + MCPClient.instance = new MCPClient(); + } + return MCPClient.instance; + } + + async connect() { + // Return existing client if already connected + if (this.client) { + return this.client; + } + + // Wait for ongoing connection if already connecting + if (this.connecting) { + return this.connecting; + } + + // Start new connection + this.connecting = this._connect(); + + try { + this.client = await this.connecting; + return this.client; + } finally { + this.connecting = null; + } + } + + private async _connect(): Promise { + // Spawn MCP server (mongodb-mcp-server) + const transport = new StdioClientTransport({ + command: "npx", + args: ["-y", "mongodb-mcp-server@latest", "--readOnly"], + env: { + ...process.env, + MDB_MCP_CONNECTION_STRING: process.env.mongo_uri!, + } + }); + + const client = new Client( + { + name: "instaviz-mcp-client", + version: "1.0.0", + }, + { + capabilities: {}, + } + ); + + await client.connect(transport); + console.log("✓ MCP Connected to MongoDB"); + + return client; + } + + async call(toolName: string, args: any = {}) { + try { + const client = await this.connect(); + + console.log(`Calling MCP tool: ${toolName}`); + console.log('Arguments:', JSON.stringify(args, null, 2)); + + const result = await client.callTool({ + name: toolName, + arguments: args, + }); + + console.log(`MCP tool ${toolName} completed successfully`); + + // Check if result has content + if (result && result.content) { + console.log('Result type:', typeof result.content); + console.log('Result preview:', JSON.stringify(result.content).slice(0, 200)); + } + + return result; + + } catch (err: any) { + console.error(` MCP tool call failed for ${toolName}:`, err); + console.error('Error details:', { + message: err.message, + name: err.name, + stack: err.stack?.slice(0, 200) + }); + throw err; + } + } + + async listTools() { + try { + const client = await this.connect(); + const tools = await client.listTools(); + + const toolDetails = tools.tools.map((t: any) => ({ + name: t.name, + description: t.description || 'No description', + inputSchema: t.inputSchema + })); + + console.log("MCP Available Tools:", toolDetails.map(t => t.name).join(', ')); + return toolDetails; + + } catch (err) { + console.error('Failed to list MCP tools:', err); + return []; + } + } + + async disconnect() { + if (this.client) { + try { + await this.client.close(); + console.log("MCP client disconnected"); + } catch (err) { + console.error("Error disconnecting MCP client:", err); + } + this.client = null; + } + } +} + +export default MCPClient.getInstance(); \ No newline at end of file diff --git a/src/services/r2Service.ts b/src/services/r2Service.ts new file mode 100644 index 0000000..fb15d32 --- /dev/null +++ b/src/services/r2Service.ts @@ -0,0 +1,21 @@ +import fs from 'fs'; +import { PutObjectCommand } from '@aws-sdk/client-s3'; +import { r2 } from '../config/r2Client'; + +export async function uploadCsvToR2(filepath: string, originalName: string): Promise { + const fileBuffer = fs.readFileSync(filepath); + const fileName = `${Date.now()}_${originalName}`; + + await r2.send( + new PutObjectCommand({ + Bucket: process.env.R2_BUCKET_NAME!, + Key: fileName, + Body: fileBuffer, + ContentType: 'text/csv', + }) + ); + + console.log('File uploaded to R2:', fileName); + + return `${process.env.R2_PUBLIC_URL}/${fileName}`; +} diff --git a/src/services/switchingApi.ts b/src/services/switchingApi.ts new file mode 100644 index 0000000..a1a0bd2 --- /dev/null +++ b/src/services/switchingApi.ts @@ -0,0 +1,66 @@ +import { GoogleGenerativeAI, GenerativeModel } from "@google/generative-ai"; + +let apiKeyIndex = 0; +const apiKeys = process.env.GEMINI_API_KEY!.split(",").map(k => k.trim()); + +let currentApi = apiKeys[apiKeyIndex]; + +// current model +let genAI = new GoogleGenerativeAI(currentApi); +let model: GenerativeModel = genAI.getGenerativeModel({ + model: "gemini-2.5-flash" +}); + +export const getModel = () => model; +export const getApiKeyCount = () => apiKeys.length; + +export const switchApi = () => { + apiKeyIndex = (apiKeyIndex + 1) % apiKeys.length; + currentApi = apiKeys[apiKeyIndex]; + + genAI = new GoogleGenerativeAI(currentApi); + model = genAI.getGenerativeModel({ model: "gemini-2.5-flash" }); + + console.log("Switched to API key:", currentApi); +}; + +export async function runAi(prompt: string) { + let attempts = 0; + + while (attempts < apiKeys.length) { + try { + console.log(`Using Gemini Key #${apiKeyIndex + 1}`); + + const res = await model.generateContent(prompt); + + let raw = res.response.text().replace(/```json|```/g, "").trim(); + const start = raw.indexOf("{"); + const end = raw.lastIndexOf("}"); + if (start === -1 || end === -1) throw new Error("Invalid JSON"); + + return JSON.parse(raw.substring(start, end + 1)); + } catch (err: any) { + const msg = String(err?.message || ""); + + console.log("Gemini Error:", msg); + + if ( + msg.includes("quota") || + msg.includes("429") || + msg.includes("exceeded") || + err.status === 503 + ) { + console.log("AI limit hit → switching key..."); + switchApi(); + attempts++; + continue; + } + + console.log("Non-limit AI failure. Stopping AI."); + return null; + } + } + + console.log("All AI keys failed"); + return null; +} diff --git a/src/services/validation.ts b/src/services/validation.ts index 2896850..6fcafd0 100644 --- a/src/services/validation.ts +++ b/src/services/validation.ts @@ -24,6 +24,33 @@ export const theValidation = Joi.object({ }); export const loginSchema = Joi.object({ - name:Joi.string().required().min(3).max(20), + email:Joi.string().required().email(), password:Joi.string().required().min(8) - }) \ No newline at end of file + }) + + export const resetPasswordSchema = Joi.object({ + email: Joi.string().email().required(), + + newPassword: Joi.string() + .pattern( + new RegExp( + "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[@$!%*#?&])[A-Za-z\\d@$!%*#?&]{8,}$" + ) + ) + .min(8) + .required() + .messages({ + "string.pattern.base": + "Password must contain letters, numbers, and symbols", + "string.min": "Password must be at least 8 characters", + "any.required": "Password is required", + }), + + confirmPassword: Joi.string() + .valid(Joi.ref("newPassword")) + .required() + .messages({ + "any.only": "Password and Confirm Password must match", + "any.required": "Confirm Password is required", + }), +}); diff --git a/src/types/charts.ts b/src/types/charts.ts new file mode 100644 index 0000000..281bf54 --- /dev/null +++ b/src/types/charts.ts @@ -0,0 +1,52 @@ +export type ColumnType = 'numeric' | 'categorical' | 'date' | 'text'; + +export interface ColumnAnalysis { + name: string; + type: ColumnType; + uniqueCount: number; + nullCount: number; + sampleValues: any[]; + variance?: number; + avgLength?: number; +} + +export interface BarPoint { + xValue: string; + yValue: number; +} + +export interface PiePoint { + xValue: string; + value: number; +} + +export interface ChartColumns { + barChartCategory: string; + barChartNumeric: string; + pieChartCategory: string; +} + +export interface GeneratedCharts { + barData: BarPoint[]; + pieData: PiePoint[]; + columns: ChartColumns; +} + + +// src/types/chart.ts +export interface IChartPoint { + xValue?: string; + yValue?: number; + value?: number; + rawValue?: number; + formattedValue?: string; +} + +export interface IChartData { + type: "bar" | "pie" | "line"; + title: string; + x?: string; + y?: string; + data: IChartPoint[]; + style?: Record; +} diff --git a/src/types/express.d.ts b/src/types/express.d.ts new file mode 100644 index 0000000..d5adb4d --- /dev/null +++ b/src/types/express.d.ts @@ -0,0 +1,17 @@ +// src/types/express.d.ts +import 'express'; + +declare global { + namespace Express { + interface UserPayload { + userId: string; + isGuest?: boolean; + } + + interface Request { + user?: UserPayload; + } + } +} + +export {}; diff --git a/src/types/express/index.d.ts b/src/types/express/index.d.ts new file mode 100644 index 0000000..9336d42 --- /dev/null +++ b/src/types/express/index.d.ts @@ -0,0 +1,10 @@ +import "express-serve-static-core"; + +declare module "express-serve-static-core" { + interface Request { + user?: { + userId: string; + isGuest?: boolean; + }; + } +} diff --git a/src/types/session.ts b/src/types/session.ts new file mode 100644 index 0000000..fe27621 --- /dev/null +++ b/src/types/session.ts @@ -0,0 +1,65 @@ +// models/Session.ts +import mongoose, { Schema, Document, Types } from "mongoose"; + +export interface ISessionMessage { + fromAi?: string | null; + fromUser?: string | null; + createdAt?: Date; +} + +export interface IChartData { + title: string; + type: "bar" | "pie" | "line"; + x?: string; + y?: string; + data: any[]; + style?: any; +} + +export interface ISession extends Document { + user_id?: Types.ObjectId | null; // Logged in user + session_token?: string | null; // Guest user session + data_id?: Types.ObjectId | null; + title: string; + messages: ISessionMessage[]; + charts: IChartData[]; + metrics: any; + createdAt: Date; + updatedAt: Date; +} + +const SessionSchema = new Schema( + { + user_id: { type: Schema.Types.ObjectId, ref: "users", default: null }, + session_token: { type: String, default: null }, // GUEST SUPPORT + + data_id: { type: Schema.Types.ObjectId, ref: "datas", default: null }, + + title: { type: String, default: "New Session" }, + + messages: [ + { + fromAi: String, + fromUser: String, + createdAt: { type: Date, default: () => new Date() }, + }, + ], + + charts: [ + { + title: String, + type: String, + x: String, + y: String, + data: Schema.Types.Mixed, + style: Schema.Types.Mixed, + }, + ], + + metrics: { type: Schema.Types.Mixed, default: {} }, + }, + { timestamps: true } +); + +export const SessionModel = + mongoose.models.sessions || mongoose.model("sessions", SessionSchema); diff --git a/src/utils/CustomError.ts b/src/utils/CustomError.ts new file mode 100644 index 0000000..1059466 --- /dev/null +++ b/src/utils/CustomError.ts @@ -0,0 +1,19 @@ +export class CustomError extends Error { + statusCode: number; + errorData: any; + success: boolean = false; + + constructor({ + message, + statusCode, + errorData, + }: { + message?: string; + statusCode?: number; + errorData?: any; + }) { + super(message || 'Iternal Server error'); + this.statusCode = statusCode || 500; + this.errorData = errorData; + } +} diff --git a/src/utils/aiPrompt.ts b/src/utils/aiPrompt.ts new file mode 100644 index 0000000..eebdc56 --- /dev/null +++ b/src/utils/aiPrompt.ts @@ -0,0 +1,129 @@ +export const generateAiPrompt = (computedMetrics: any, dataset: any, tools: any) => { + const toolNames = Array.isArray(tools) + ? tools.map((t: any) => (typeof t === "string" ? t : t.name)).join(", ") + : String(tools); + + const datasetIdString = dataset._id.toString(); + const sample = dataset.sample_data || []; + + return `You are an expert data analyst for InstaviZ with 10 year experience. Analyze the provided CSV dataset and generate insights with charts. + + this model is created by instaviz dont forget understood! + use your 100% iq to do this. + +DATASET INFORMATION: +- Dataset ID: ${datasetIdString} +- R2 File URL: ${dataset.r2_url} +- Total Rows: ${computedMetrics.total_rows} +- Total Columns: ${computedMetrics.total_columns} +- Missing Values: ${computedMetrics.missing_values} + +SAMPLE DATA (first 10 rows): +${JSON.stringify(sample, null, 2)} + +AVAILABLE MCP TOOLS: +${toolNames} + +INSTRUCTIONS: +1. Analyze the dataset structure and identify: + - Categorical columns (for grouping) + - Numerical columns (for aggregation) + - Key fields and patterns + +2. Generate TWO charts: + a) BAR CHART: Use categorical grouping with numerical aggregation + b) PIE CHART: Use categorical grouping with counts + +3. All rows are stored in the MongoDB collection "dataset_rows". + - Each document in "dataset_rows" represents one row of the CSV. + - Each document has a field "datasetId" (string) equal to "${datasetIdString}". + - CSV columns are stored as top-level fields (e.g. "industry", "size", "value", etc). + +4. For numeric calculations, you MUST use MCP tools (especially "aggregate"). + +EXAMPLE MCP AGGREGATIONS (ADAPT COLUMN NAMES): + +FOR BAR CHART (sum of numeric column by category): +{ + "database": "Instaviz", + "collection": "dataset_rows", + "pipeline": [ + { "$match": { "datasetId": "${datasetIdString}" } }, + { + "$group": { + "_id": "$CATEGORY_COLUMN_NAME", + "value": { "$sum": { "$toDouble": "$NUMERIC_COLUMN_NAME" } } + } + }, + { "$project": { "_id": 0, "xValue": "$_id", "yValue": "$value" } }, + { "$sort": { "yValue": -1 } }, + { "$limit": 10 } + ] +} + +FOR PIE CHART (count by category): +{ + "database": "Instaviz", + "collection": "dataset_rows", + "pipeline": [ + { "$match": { "datasetId": "${datasetIdString}" } }, + { + "$group": { + "_id": "$CATEGORY_COLUMN_NAME", + "value": { "$sum": 1 } + } + }, + { "$project": { "_id": 0, "xValue": "$_id", "value": "$value" } }, + { "$sort": { "value": -1 } }, + { "$limit": 8 } + ] +} + +CRITICAL RULES: +- You MUST call MCP tools (especially "aggregate") to get real chart data. +- After receiving MCP results, you MUST map them into the JSON structure described below. +- "type" MUST be exactly "bar" for the bar chart and "pie" for the pie chart. +- For bar charts, each point MUST have "xValue" and "yValue". +- For pie charts, each point MUST have "xValue" and "value". +- Replace CATEGORY_COLUMN_NAME and NUMERIC_COLUMN_NAME with actual column names from the dataset. +- NEVER return placeholder or dummy data. +- Output ONLY valid JSON. NO markdown, NO backticks, NO extra text before or after the JSON. + +REQUIRED OUTPUT FORMAT: +{ + "metrics": { + "total_rows": ${computedMetrics.total_rows}, + "total_columns": ${computedMetrics.total_columns}, + "missing_values": ${computedMetrics.missing_values}, + "charts_generated": 2 + }, + "charts": [ + { + "type": "bar", + "title": "Descriptive Title", + "x": "category_column_name", + "y": "numeric_column_name", + "data": [ + { "xValue": "category1", "yValue": 123.45 } + ] + }, + { + "type": "pie", + "title": "Descriptive Title", + "x": "category_column_name", + "y": "count", + "data": [ + { "xValue": "category1", "value": 50 } + ] + } + ], + "summary": [ + "Key insight 1 about the data", + "Key insight 2 about patterns", + "Key insight 3 about trends" + ], + "key_fields": ["field1", "field2", "field3"] +} + +Begin analysis now. Use MCP tools to fetch real data for charts and then respond ONLY with the final JSON object described above.`; +}; diff --git a/src/utils/chartHelpers.ts b/src/utils/chartHelpers.ts new file mode 100644 index 0000000..3083171 --- /dev/null +++ b/src/utils/chartHelpers.ts @@ -0,0 +1,91 @@ +import type { Aggregations } from "./streamAggregations"; + +export type FinalChart = { + type: "bar" | "pie" | "line"; + title: string; + x: string; + y?: string; + data: Array<{ xValue: string; yValue?: number; value?: number }>; +}; + +export function generateChartData( + aiCharts: any[] | null, + aggregations: Aggregations +): FinalChart[] { + if (!aiCharts || aiCharts.length === 0) return []; + + const finalCharts: FinalChart[] = []; + + for (const chart of aiCharts) { + if (chart.type !== "bar" && chart.type !== "pie") continue; + + // BAR CHART + if (chart.type === "bar") { + const xCol = chart.x; + const yCol = + chart.y && chart.y !== "count" + ? chart.y + : Object.keys(aggregations.numeric)[0]; + + const cat = aggregations.categorical[xCol]; + if (!cat) continue; + + const rows = Object.entries(cat.counts).map(([label, count]) => ({ + xValue: label, + yValue: Number(count), // <-- FIXED (no more unknown) + })); + + finalCharts.push({ + type: "bar", + title: chart.title, + x: xCol, + y: yCol, + data: rows, + }); + } + + // PIE CHART + if (chart.type === "pie") { + const xCol = chart.x; + const cat = aggregations.categorical[xCol]; + if (!cat) continue; + + const rows = Object.entries(cat.counts).map(([name, count]) => ({ + xValue: name, + value: Number(count), // <-- FIXED + })); + + finalCharts.push({ + type: "pie", + title: chart.title, + x: xCol, + data: rows, + }); + } + + // LINE CHART + if (chart.type === "line") { + const xCol = chart.x; + const yCol = chart.y; + + const cat = aggregations.categorical[xCol]; + if (!cat) continue; + + const rows = Object.entries(cat.counts).map(([label, count]) => ({ + xValue: label, + yValue: Number(count), + })); + + finalCharts.push({ + type: "line", + title: chart.title, + x: xCol, + y: yCol, + data: rows, + }); + } + } + + + return finalCharts; +} diff --git a/src/utils/chartInterpreter.ts b/src/utils/chartInterpreter.ts new file mode 100644 index 0000000..f476f03 --- /dev/null +++ b/src/utils/chartInterpreter.ts @@ -0,0 +1,144 @@ +// utils/chartInterpreter.ts + +import type { Aggregations } from "./streamAggregations"; + +export interface ChartIntent { + valid: boolean; + type: "bar" | "pie" | "line" | null; + category: string | null; + numeric: string | null; +} + +// INTERPRET AI CHART REQUEST +export function interpretChartRequest( + chartReq: any, + aggregations: Aggregations +): ChartIntent { + if (!chartReq) { + return { valid: false, type: null, category: null, numeric: null }; + } + + const category = chartReq.category?.trim() || null; + const numeric = chartReq.numeric?.trim() || null; + const type = chartReq.chart_type || null; + + // Validate columns exist in dataset + const allCols = [ + ...Object.keys(aggregations.categorical), + ...Object.keys(aggregations.numeric), + ]; + + const isValidCategory = category && allCols.includes(category); + const isValidNumeric = + !numeric || Object.keys(aggregations.numeric).includes(numeric); + + if (!type || !isValidCategory) { + return { valid: false, type: null, category: null, numeric: null }; + } + + // type now supports line + if (!["bar", "pie", "line"].includes(type)) { + return { valid: false, type: null, category: null, numeric: null }; + } + + return { + valid: true, + type, + category, + numeric, + }; +} + +// UNIVERSAL FALLBACK CHART GENERATOR +export function fallbackChartGenerator( + intent: ChartIntent, + aggregations: Aggregations +) { + const { type, category, numeric } = intent; + + // PIE + if (type === "pie") { + const cat = aggregations.categorical[category!]; + if (!cat) + return { type: "pie", title: `Distribution of ${category}`, x: category!, data: [] }; + + const rows = Object.entries(cat.counts).map(([name, count]) => ({ + xValue: name, + value: Number(count), + })); + + return { + type: "pie", + title: `Distribution of ${category}`, + x: category!, + data: rows, + }; + } + + // BAR + if (type === "bar") { + let yCol = numeric; + + if (!yCol || yCol === "count") { + yCol = Object.keys(aggregations.numeric)[0]; // default numeric column + } + + const cat = aggregations.categorical[category!]; + if (!cat) + return { + type: "bar", + title: `${yCol} by ${category}`, + x: category!, + y: yCol!, + data: [], + }; + + const rows = Object.entries(cat.counts).map(([name, count]) => ({ + xValue: name, + yValue: count, + })); + + return { + type: "bar", + title: `${yCol} by ${category}`, + x: category!, + y: yCol!, + data: rows, + }; + } + + // LINE (NEW) + if (type === "line") { + let yCol = numeric; + + if (!yCol || yCol === "count") { + yCol = Object.keys(aggregations.numeric)[0]; + } + + const cat = aggregations.categorical[category!]; + if (!cat) + return { + type: "line", + title: `${yCol} Trend by ${category}`, + x: category!, + y: yCol!, + data: [], + }; + + // Turn categorical distribution into a time-like series + const rows = Object.entries(cat.counts).map(([name, count]) => ({ + xValue: name, + yValue: count, + })); + + return { + type: "line", + title: `${yCol} Trend for ${category}`, + x: category!, + y: yCol!, + data: rows, + }; + } + + return null; +} diff --git a/src/utils/chartNormalizer.ts b/src/utils/chartNormalizer.ts new file mode 100644 index 0000000..e033783 --- /dev/null +++ b/src/utils/chartNormalizer.ts @@ -0,0 +1,48 @@ +// utils/chartNormalizer.ts +export function normalizeChartsForFrontend(aiCharts: any[]): any[] { + if (!Array.isArray(aiCharts)) return []; + + return aiCharts + .map((chart: any) => { + const rawType = String(chart.type || '').toLowerCase(); + + const type = + rawType.includes('bar') ? 'bar' : + rawType.includes('pie') ? 'pie' : + rawType.includes('line') ? 'line' : + null; + + if (!type) return null; + + const x = chart.x || chart.xField || chart.categoryField || 'x'; + const y = chart.y || chart.yField || chart.valueField || 'y'; + const title = chart.title || `${type.toUpperCase()} chart`; + + const rawData = chart.data || chart.points || chart.series || []; + + const data = rawData.map((p: any) => { + const xValue = + p.xValue ?? + p[x] ?? + p.label ?? + p.name ?? + p.category ?? + ''; + + const yValue = + p.yValue ?? + p[y] ?? + p.value ?? + null; + + return { + xValue, + yValue, + value: p.value ?? yValue, + }; + }); + + return { type, x, y, title, data }; + }) + .filter(Boolean); +} diff --git a/src/utils/chartPrompt.ts b/src/utils/chartPrompt.ts new file mode 100644 index 0000000..1d9b995 --- /dev/null +++ b/src/utils/chartPrompt.ts @@ -0,0 +1,217 @@ +// utils/chartPrompt.ts +import type { IDataset } from "../model/dataModel"; + +export function generateChartPrompt(userMessage: string, dataset: IDataset) { + return ` +You are InstaviZ's senior data analyst and chart engine with 10 year experience. +this model is created by instaviz dont forget understood! + +You receive: +- A dataset (stored in MongoDB in collection "dataset_rows"). +- The user's natural language request. +- A sample of the dataset rows. + +Your job is to: +1. Understand what the user wants. +2. Choose an appropriate chart type. +3. Choose good columns automatically (NO follow-up questions). +4. Use MCP tools (MongoDB "aggregate") to fetch real data. +5. Return ONLY a JSON object with a natural language reply + a single chart spec. + +──────────────── DATA CONTEXT ──────────────── +- Dataset ID: ${dataset._id} +- MongoDB database: "Instaviz" +- MongoDB collection: "dataset_rows" +- Each row document looks like: + { datasetId: "", ...original CSV columns } + +SAMPLE ROWS (from the dataset): +${JSON.stringify(dataset.sample_data.slice(0, 10), null, 2)} + +USER MESSAGE: +"${userMessage}" + +──────────────── INTERPRET USER INTENT ──────────────── +First, interpret what the user is asking: + +- If they clearly ask for **a specific chart type**: + - "pie chart", "donut chart", "share", "percentage breakdown" → use type = "pie" + - "line chart", "trend", "over time", "time series", "evolution" → use type = "line" + - "bar chart", "column chart", "compare categories", "top N" → use type = "bar" + +- If they just say "create a chart", "visualize this", "show me something": + - You must choose the best chart type based on the dataset and message: + - Prefer **line** only if a clear date / time / ordered numeric dimension exists AND the user implies a trend. + - Prefer **pie** only when showing distribution over a small number of categories (3–10) and the idea is "share" or "proportion". + - Otherwise default to **bar** for categorical comparison. + +- If the user is NOT asking you to create a chart (e.g., "what is a pie chart?"): + - Explain the concept briefly and set "chart": null in the output. + +For this integration, the backend only calls you when the intent is about creating or modifying a chart, so usually you DO need to return a chart. + +──────────────── COLUMN SELECTION RULES ──────────────── +You MUST NOT ask follow-up questions like "which column should be x?". +You must infer the best columns from the data. + +You have 3 types of columns: +- Date / time-like: + - Column names like: date, day, month, year, period, quarter, created_at, timestamp, time, week, etc. + - Values look like dates or years (e.g. "2023-01-01", "2023", "Q1 2023"). + +- Numeric: + - Values can be converted with $toDouble. + - Names often contain: value, amount, total, revenue, sales, volume, count, quantity, score, rating, etc. + +- Categorical: + - Text labels with relatively few unique values: industry, region, size, segment, status, category, type, country, city, etc. + +Rules per chart type: + +1) LINE CHART (type = "line"): + - X-axis: + - Prefer a date/time column if available. + - If multiple date/time-like columns exist, choose the one that best matches the user's message (e.g. "year" for yearly trends, "month" for monthly). + - If no date/time column: + - Use an ordered numeric or ordinal dimension (e.g. "year", "period", "index"). + + - Y-axis: + - Choose a numeric column with meaningful variance. + - Prefer columns with names like: value, amount, revenue, sales, count, quantity, total. + - Filter out empty strings before converting to double. + + - Aggregation: + - Group by x-axis, aggregate y-axis with $sum or $avg (choose based on your understanding of the field). + - Sort by xValue ascending so the line is chronological/ordered. + +2) BAR CHART (type = "bar"): + - X-axis: Best categorical dimension (e.g. industry, size, region, category). + - Y-axis: Numeric metric (same rules as above). + - Aggregation: + - Group by x-axis and $sum or $avg the numeric metric. + - Sort descending by yValue and limit to top 10–15 categories. + +3) PIE CHART (type = "pie"): + - Only use when the user explicitly asks for a pie/donut or clearly wants a percentage breakdown. + - X-axis: categorical dimension (e.g. size, region, status, category). + - Value: count of rows or sum of a numeric metric. + - Keep number of slices small: + - Prefer 3–8 categories; group smaller ones into "Other" if needed. + +──────────────── MCP TOOL USAGE ──────────────── +Use the "aggregate" tool with: + +- database: "Instaviz" +- collection: "dataset_rows" +- Always filter rows by: { "datasetId": "${dataset._id}" } + +Examples: + +1) LINE chart (sum numeric by date/time column): + +{ + "database": "Instaviz", + "collection": "dataset_rows", + "pipeline": [ + { "$match": { "datasetId": "${dataset._id}", "value": { "$ne": "" } } }, + { + "$group": { + "_id": "$DATE_COLUMN_NAME", + "value": { "$sum": { "$toDouble": "$NUMERIC_COLUMN_NAME" } } + } + }, + { + "$project": { + "_id": 0, + "xValue": "$_id", + "yValue": "$value" + } + }, + { "$sort": { "xValue": 1 } } + ] +} + +2) BAR chart (sum numeric by category): + +{ + "database": "Instaviz", + "collection": "dataset_rows", + "pipeline": [ + { "$match": { "datasetId": "${dataset._id}", "value": { "$ne": "" } } }, + { + "$group": { + "_id": "$CATEGORY_COLUMN_NAME", + "value": { "$sum": { "$toDouble": "$NUMERIC_COLUMN_NAME" } } + } + }, + { + "$project": { + "_id": 0, + "xValue": "$_id", + "yValue": "$value" + } + }, + { "$sort": { "yValue": -1 } }, + { "$limit": 12 } + ] +} + +3) PIE chart (count by category): + +{ + "database": "Instaviz", + "collection": "dataset_rows", + "pipeline": [ + { "$match": { "datasetId": "${dataset._id}" } }, + { + "$group": { + "_id": "$CATEGORY_COLUMN_NAME", + "value": { "$sum": 1 } + } + }, + { + "$project": { + "_id": 0, + "xValue": "$_id", + "value": "$value" + } + }, + { "$sort": { "value": -1 } }, + { "$limit": 8 } + ] +} + +Replace CATEGORY_COLUMN_NAME, DATE_COLUMN_NAME, NUMERIC_COLUMN_NAME with real fields inferred from the dataset. + +──────────────── OUTPUT FORMAT (STRICT) ──────────────── +You MUST output ONLY valid JSON. NO markdown, NO backticks, NO comments. + +Shape: + +{ + "reply": "Short, helpful explanation of what you plotted and why you chose those columns and chart type.", + "chart": { + "type": "line" | "bar" | "pie", + "title": "Descriptive chart title", + "x": "name_of_x_axis_column_in_dataset", + "y": "name_of_y_axis_column_in_dataset_or_'count'_for_pie", + "data": [ + // For bar/line: + { "xValue": "x category or time", "yValue": 123.45 }, + ... + // For pie: + // { "xValue": "slice name", "value": 50 } + ] + } +} + +If, and only if, the user is clearly NOT requesting a chart but just an explanation, you may respond with: + +{ + "reply": "Some explanation...", + "chart": null +} + +Do not ask the user for more information. Make an expert best guess from the data and their message be compatative and use your maximum iq to do this. +`; +} diff --git a/src/utils/chatDetection.ts b/src/utils/chatDetection.ts new file mode 100644 index 0000000..7c646bc --- /dev/null +++ b/src/utils/chatDetection.ts @@ -0,0 +1,6 @@ +// utils/chatDetection.ts +export function userWantsChart(message: string) { + return /(chart|graph|plot|visualize|show.*trend|distribution|aggregate|group by|sum by)/i.test( + message + ); +} diff --git a/src/utils/chatPrompt.ts b/src/utils/chatPrompt.ts new file mode 100644 index 0000000..45c4eba --- /dev/null +++ b/src/utils/chatPrompt.ts @@ -0,0 +1,23 @@ +// prompts/chatPrompt.ts +export function createChatPrompt(message: string, dataset: any) { + return ` +You are InstaviZ AI. A user is chatting about their uploaded dataset. +this model is created by instaviz dont forget understood! + +Never generate charts using this model. +Never call tools. +Only answer the question. + +DATASET SUMMARY: +- Rows: ${dataset.row_count} +- Columns: ${dataset.column_count} + +SAMPLE DATA: +${JSON.stringify(dataset.sample_data, null, 2)} + +USER QUESTION: +"${message}" + +Provide a clear helpful answer in text only. +`; +} diff --git a/src/utils/deviceLogger.ts b/src/utils/deviceLogger.ts new file mode 100644 index 0000000..92a0bec --- /dev/null +++ b/src/utils/deviceLogger.ts @@ -0,0 +1,24 @@ +import { Response , Request , NextFunction } from "express"; +import { deviceModel } from "../model/admin/insights/deviceModel"; + +export const deviceLogger = async (req : Request , res : Response , next : NextFunction)=>{ + try{ + const user = (req as any).user // get the logged in user + if(!user) return next() // if not user skip logging and continue request flow + + + await deviceModel.create({ + userId : user._id, + userAgent : req.headers['user-agent'] || 'unknown', // browser + os info + ipAddress : req.ip || // request IP + (req.headers['x-forwarded-for'] as string) || // For proxy / production servers + req.socket.remoteAddress // fallback method to get IP Address + }) + + next() +} + catch (err) { + console.log("Device logging error:", err); // If logging fails, print error but dont break login flow + next(); +} +} diff --git a/src/utils/fallbackCharts.ts b/src/utils/fallbackCharts.ts new file mode 100644 index 0000000..1c1f189 --- /dev/null +++ b/src/utils/fallbackCharts.ts @@ -0,0 +1,216 @@ +// utils/fallbackCharts.ts + +export function generateChartsFromData(rows: any[]) { + console.log(" Fallback: Generating charts ..."); + + if (!rows || rows.length === 0) { + return { + barData: [], + pieData: [], + columns: { + barChartCategory: "", + barChartNumeric: "", + pieChartCategory: "", + }, + }; + } + + const sample = rows[0]; + const columns = Object.keys(sample); + + type ColumnType = "numeric" | "categorical" | "date" | "text"; + + interface ColumnAnalysis { + name: string; + type: ColumnType; + uniqueCount: number; + nullCount: number; + variance: number; + avgLength: number; + sampleValues: any[]; + } + + const analyses: ColumnAnalysis[] = columns.map((col) => { + const values = rows.map((r) => r[col]); + + const nonNull = values.filter( + (v) => v !== "" && v !== null && v !== undefined + ); + + const unique = new Set(nonNull); + + const numericValues = nonNull + .map((v) => Number(String(v).replace(/,/g, ""))) + .filter((v) => !isNaN(v)); + + const isNumeric = numericValues.length >= nonNull.length * 0.6; + + const dateRegex = + /\d{4}-\d{2}-\d{2}|\d{2}\/\d{2}\/\d{4}|\d{1,2}-\w{3}-\d{4}/; + + const hasDatePattern = nonNull.some((v) => dateRegex.test(String(v))); + + let type: ColumnType = "text"; + if (hasDatePattern) type = "date"; + else if (isNumeric) type = "numeric"; + else if (unique.size <= nonNull.length * 0.5) type = "categorical"; + + // Variance for numeric + let variance = 0; + if (numericValues.length > 0) { + const mean = + numericValues.reduce((a, b) => a + b, 0) / numericValues.length; + variance = + numericValues.reduce((sum, v) => sum + Math.pow(v - mean, 2), 0) / + numericValues.length; + } + + const avgLength = + nonNull.reduce((sum, v) => sum + String(v).length, 0) / + (nonNull.length || 1); + + return { + name: col, + type, + uniqueCount: unique.size, + nullCount: values.length - nonNull.length, + variance, + avgLength, + sampleValues: [...unique].slice(0, 5), + }; + }); + + // BAR CHART COLUMN SELECTION + const numericScores = analyses + .map((c) => { + let score = 0; + if (c.type === "numeric") score += 100; + if (c.variance > 0) score += 40; + + const keywords = ["value", "amount", "price", "total", "count", "score"]; + if (keywords.some((k) => c.name.toLowerCase().includes(k))) score += 40; + + if (c.name.toLowerCase().includes("id")) score -= 100; + if (c.name.toLowerCase().includes("reference")) score -= 70; + + return { col: c.name, score }; + }) + .sort((a, b) => b.score - a.score); + + const bestNumeric = numericScores[0]?.col; + + const categoryScores = analyses + .map((c) => { + let score = 0; + if (c.type === "categorical") score += 100; + if (c.uniqueCount >= 2 && c.uniqueCount <= 50) score += 50; + + const keywords = [ + "category", + "type", + "status", + "region", + "gender", + "level", + "group", + ]; + if (keywords.some((k) => c.name.toLowerCase().includes(k))) score += 40; + + if (c.name.toLowerCase().includes("id")) score -= 100; + if (c.name.toLowerCase().includes("reference")) score -= 70; + + return { col: c.name, score }; + }) + .sort((a, b) => b.score - a.score); + + const bestCategory = categoryScores[0]?.col; + + // STRICT, MEANINGFUL PIE CHART COLUMN SELECTION + + const pieCandidates = analyses.filter((c) => { + return ( + c.type === "categorical" && + c.uniqueCount >= 3 && + c.uniqueCount <= 20 && // LIMIT unique count + c.avgLength <= 25 && // avoid huge text columns + !c.name.toLowerCase().includes("id") && + !c.name.toLowerCase().includes("ref") && + !c.name.toLowerCase().includes("reference") && + !c.name.toLowerCase().includes("series") && + !c.name.toLowerCase().includes("code") + ); + }); + + const keywordPriority = ["status", "type", "category", "region", "gender", "group", "level"]; + pieCandidates.sort((a, b) => { + const aScore = keywordPriority.some((k) => a.name.toLowerCase().includes(k)) ? 1 : 0; + const bScore = keywordPriority.some((k) => b.name.toLowerCase().includes(k)) ? 1 : 0; + return bScore - aScore; + }); + + const bestPieCategory = pieCandidates.length > 0 ? pieCandidates[0].name : null; + + //BAR CHART GENERATION + + const barMap = new Map(); + + rows.forEach((r) => { + const cat = String(r[bestCategory] || "").trim(); + const num = Number(String(r[bestNumeric]).replace(/,/g, "")); + if (!cat || isNaN(num)) return; + + barMap.set(cat, (barMap.get(cat) || 0) + num); + }); + + const barData = [...barMap.entries()] + .map(([xValue, yValue]) => ({ xValue, yValue })) + .sort((a, b) => b.yValue - a.yValue) + .slice(0, 15); + + // PIE CHART GENERATION (only if valid) + + if (!bestPieCategory) { + return { + barData, + pieData: [], + columns: { + barChartCategory: bestCategory, + barChartNumeric: bestNumeric, + pieChartCategory: null, + }, + }; + } + + const pieMap = new Map(); + + rows.forEach((r) => { + const cat = String(r[bestPieCategory] || "").trim(); + if (!cat) return; + pieMap.set(cat, (pieMap.get(cat) || 0) + 1); + }); + + let pieEntries = [...pieMap.entries()].sort((a, b) => b[1] - a[1]); + + let pieData = pieEntries.slice(0, 8).map(([xValue, value]) => ({ + xValue, + value, + })); + + if (pieEntries.length > 8) { + const other = pieEntries + .slice(8) + .reduce((sum, [, v]) => sum + v, 0); + + pieData.push({ xValue: "Other", value: other }); + } + + return { + barData, + pieData, + columns: { + barChartCategory: bestCategory, + barChartNumeric: bestNumeric, + pieChartCategory: bestPieCategory, + }, + }; +} diff --git a/src/utils/featureLogger.ts b/src/utils/featureLogger.ts new file mode 100644 index 0000000..9f747e9 --- /dev/null +++ b/src/utils/featureLogger.ts @@ -0,0 +1,2 @@ +import { Response , Request , NextFunction } from "express"; +import { FeatureUsageModel } from "../model/admin/insights/featureModel"; \ No newline at end of file diff --git a/src/utils/hashTokens.ts b/src/utils/hashTokens.ts new file mode 100644 index 0000000..742e196 --- /dev/null +++ b/src/utils/hashTokens.ts @@ -0,0 +1,5 @@ +import crypto from "crypto"; + +export const hashToken = (token: string): string => { + return crypto.createHash("sha256").update(token).digest("hex"); //always converts input into a 64-character hexadecimal string +}; diff --git a/src/utils/imageUploader.ts b/src/utils/imageUploader.ts new file mode 100644 index 0000000..a7a9273 --- /dev/null +++ b/src/utils/imageUploader.ts @@ -0,0 +1,38 @@ +import { supabase } from "../config/supabaseClient"; + +const ALLOWED_MIME_TYPES = ["image/png", "image/jpeg", "image/jpg"]; + +export const uploadimageToSupabase = async (userId: string, image: string) => { + console.log("api reached here"); + if (!image.startsWith("data:image/")) { + throw new Error("Invalid image format. Must be base64 encoded image."); + } + + const mimeType = image.substring(image.indexOf(":") + 1, image.indexOf(";")); + + if (!ALLOWED_MIME_TYPES.includes(mimeType)) { + throw new Error("Unsupported image type. Only PNG and JPG are allowed."); + } + + const base64Data = image.replace(/^data:image\/\w+;base64,/, ""); + const buffer = Buffer.from(base64Data, "base64"); + + const extension = mimeType.split("/")[1]; + const fileName = `user-${userId}-${Date.now()}.${extension}`; + + const { data, error } = await supabase.storage + .from("picture") + .upload(fileName, buffer, { + cacheControl: "3600", + upsert: true, + contentType: mimeType, + }); + + if (error) throw error; + + const { data: publicUrlData } = supabase.storage + .from("picture") + .getPublicUrl(fileName); + + return publicUrlData.publicUrl; +}; diff --git a/src/utils/loadDatasetFromDB.ts b/src/utils/loadDatasetFromDB.ts new file mode 100644 index 0000000..66e9b15 --- /dev/null +++ b/src/utils/loadDatasetFromDB.ts @@ -0,0 +1,21 @@ +// utils/loadDatasetFromDB.ts +import dataModel from "../model/dataModel"; + +export async function loadDatasetFromDB() { + try { + const latest = await dataModel + .findOne({}) + .sort({ createdAt: -1 }) + .lean(); + + if (!latest) return null; + + return { + sampleRows: latest.data || [], + aggregations: (latest as any).aggregations || null, + }; + } catch (err) { + console.error("Failed to load dataset:", err); + return null; + } +} diff --git a/src/utils/multerUpload.ts b/src/utils/multerUpload.ts new file mode 100644 index 0000000..d9343bf --- /dev/null +++ b/src/utils/multerUpload.ts @@ -0,0 +1,43 @@ +import multer, { FileFilterCallback } from 'multer'; +import path from 'path'; +import fs from 'fs'; +import { Request } from 'express'; + +const UPLOAD_DIR = path.join(process.cwd(), 'uploads'); +if (!fs.existsSync(UPLOAD_DIR)) { + fs.mkdirSync(UPLOAD_DIR, { recursive: true }); + console.log('Created upload directory:', UPLOAD_DIR); +} + +const storage = multer.diskStorage({ + destination: function (_req:Request, _file, cb) { + cb(null, UPLOAD_DIR); + }, + filename: function (_req:Request, file, cb) { + const suffix = Date.now() + '_' + Math.round(Math.random() * 1e9); + const safeName = file.originalname.replace(/[^a-zA-Z0-9.\-_]/g, '_'); + cb(null, `${suffix}_${safeName}`); + }, +}); + +function fileFiltercsv(_req: Request, file: Express.Multer.File, cb: FileFilterCallback) { + const ext = path.extname(file.originalname).toLowerCase(); + if (ext !== '.csv') { + return cb(new Error('Only CSV file type allowed')); + } + if (file.mimetype !== 'text/csv' && file.mimetype !== 'application/vnd.ms-excel') { + return cb(new Error('Invalid mimetype for CSV')); + } + cb(null, true); +} + +const MAX_FILE_SIZE = 100 * 1024 * 1024; + +const upload = multer({ + storage:storage, + fileFilter: fileFiltercsv, + limits: { fileSize: MAX_FILE_SIZE, files: 1 }, +}); + +export default upload; +export { UPLOAD_DIR }; diff --git a/src/utils/sendEmail.ts b/src/utils/sendEmail.ts index 2731e94..7bf4372 100644 --- a/src/utils/sendEmail.ts +++ b/src/utils/sendEmail.ts @@ -1,28 +1,36 @@ -import nodemailer from 'nodemailer' -export const sendOtp = async(email:string,otp:string) =>{ - console.log("reached here at send otp"); - - try{ - let sendMail = nodemailer.createTransport({ - service:"gmail", - auth:{ - user:process.env.EMAIL, - pass:process.env.EMAIL_PASS - } - }) +import nodemailer from "nodemailer"; - const info = await sendMail.sendMail({ - from:"shanuvr.work.org@gmail.com", - to:email, - subject:"your OTP for InstaViz", - text:`your otp is ${otp}` - }) - console.log("opt sentttt"); +export const sendOtp = async (email: string, otp: string) => { + try { + const transporter = nodemailer.createTransport({ + service: "gmail", + auth: { + user: process.env.EMAIL, + pass: process.env.EMAIL_PASS, + }, + }); - }catch(err){ - console.log("eroor woeked"); - console.log("error sending email",err) - - } - -} + await transporter.sendMail({ + from: "itsteamnextra@gmail.com", + to: email, + subject: "Here’s your verification code ", + text: ` +Hey there! + +Just confirming it's you. +Here’s your OTP: + + ${otp} + +This code will be valid for the next 10 minutes. + +If you didn’t request this, feel free to ignore this email. +– InstaviZ + `, + }); + + console.log("OTP sent"); + } catch (err) { + console.log("Error sending OTP:", err); + } +}; diff --git a/src/utils/streamAggregations.ts b/src/utils/streamAggregations.ts new file mode 100644 index 0000000..cd92506 --- /dev/null +++ b/src/utils/streamAggregations.ts @@ -0,0 +1,170 @@ +// utils/streamAggregations.ts +export type Row = Record; + +export type NumericSummary = { + sum: number; + min: number; + max: number; + count: number; + missing: number; + unique: Set; +}; + +export type CategoricalSummary = { + counts: Map; + missing: number; + unique: Set; +}; + +export type StreamingAggState = { + rowCount: number; + numericCols: Set; + categoricalCols: Set; + columnDetectionLimit: number; + detectionSamples: number; + numeric: Record; + categorical: Record; +}; + +const tryParseNumber = (v: any) => { + if (v === null || v === undefined) return NaN; + const s = String(v).trim(); + if (!s) return NaN; + const cleaned = s.replace(/,/g, ""); + const n = Number(cleaned); + return Number.isFinite(n) ? n : NaN; +}; + +export const initStreamingAgg = (): StreamingAggState => { + return { + rowCount: 0, + numericCols: new Set(), + categoricalCols: new Set(), + detectionSamples: 0, + columnDetectionLimit: 500, + numeric: {}, + categorical: {}, + }; +}; + +const detectColumnTypes = (state: StreamingAggState, row: Row) => { + state.detectionSamples++; + + const cols = Object.keys(row); + for (const col of cols) { + const val = row[col]; + const n = tryParseNumber(val); + + if (!state.numeric[col]) { + state.numeric[col] = { + sum: 0, + min: Number.POSITIVE_INFINITY, + max: Number.NEGATIVE_INFINITY, + count: 0, + missing: 0, + unique: new Set(), + }; + } + if (!state.categorical[col]) { + state.categorical[col] = { + counts: new Map(), + missing: 0, + unique: new Set(), + }; + } + + if (!Number.isNaN(n)) { + state.numericCols.add(col); + } else { + state.categoricalCols.add(col); + } + } +}; + +const finalizeColumnTypes = (state: StreamingAggState) => { + for (const col of state.numericCols) state.categoricalCols.delete(col); +}; + +export const updateStreamingAgg = (state: StreamingAggState, row: Row) => { + state.rowCount++; + + if (state.detectionSamples < state.columnDetectionLimit) { + detectColumnTypes(state, row); + return; + } + + // lock types after detection phase + if (state.detectionSamples === state.columnDetectionLimit) { + finalizeColumnTypes(state); + } + + // Now do full aggregation + for (const col of Object.keys(row)) { + const raw = row[col]; + const trimmed = raw === null || raw === undefined ? "" : String(raw).trim(); + + if (state.numericCols.has(col)) { + const n = tryParseNumber(raw); + const ref = state.numeric[col]; + + if (Number.isNaN(n)) { + ref.missing++; + } else { + ref.sum += n; + ref.count++; + if (n < ref.min) ref.min = n; + if (n > ref.max) ref.max = n; + ref.unique.add(trimmed); + } + } else { + const ref = state.categorical[col]; + + if (!trimmed) { + ref.missing++; + } else { + const prev = ref.counts.get(trimmed) || 0; + ref.counts.set(trimmed, prev + 1); + ref.unique.add(trimmed); + } + } + } +}; + +export const finalizeStreamingAgg = (state: StreamingAggState) => { + const finalNumeric: any = {}; + for (const col of state.numericCols) { + const ref = state.numeric[col]; + finalNumeric[col] = { + sum: ref.sum, + min: ref.min === Infinity ? 0 : ref.min, + max: ref.max === -Infinity ? 0 : ref.max, + avg: ref.count > 0 ? ref.sum / ref.count : 0, + count: ref.count, + missing: ref.missing, + unique_count: ref.unique.size, + }; + } + + const finalCategorical: any = {}; + for (const col of state.categoricalCols) { + const ref = state.categorical[col]; + const entries = [...ref.counts.entries()].sort((a, b) => b[1] - a[1]); + + finalCategorical[col] = { + counts: Object.fromEntries(entries), + unique_count: ref.unique.size, + missing: ref.missing, + top_values: entries.slice(0, 10).map(([value, count]) => ({ value, count })), + }; + } + + return { + numeric: finalNumeric, + categorical: finalCategorical, + meta: { + total_rows: state.rowCount, + total_columns: state.numericCols.size + state.categoricalCols.size, + }, + }; +}; +export type Aggregations = ReturnType; \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 8e17335..8037f25 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,7 @@ "compilerOptions": { "target": "ES2020", "module": "ESNext", + "moduleResolution": "bundler", "outDir": "./dist", "rootDir": "./src", @@ -12,8 +13,9 @@ "allowImportingTsExtensions": true }, "include": [ - "src/**/*", - "src/services" + "src/*/", + "src/services", + "src/types/express.d.ts" ], "exclude": [ "node_modules"