diff --git a/backend/middleware/rateLimiter.js b/backend/middleware/rateLimiter.js index 3533274..a4ba3eb 100644 --- a/backend/middleware/rateLimiter.js +++ b/backend/middleware/rateLimiter.js @@ -104,6 +104,13 @@ const burstProtection = createUserBasedRateLimiter( "Too many requests in a short period. Please slow down." ); +// Upload presign rate limiter - 30 requests per minute +const uploadPresignLimiter = createUserBasedRateLimiter( + 60 * 1000, // 1 minute window + 30, // 30 presign requests per minute per user + "Too many upload requests. Please slow down." +); + // Authentication rate limiters const authRateLimiters = { // Login rate limiter - stricter to prevent brute force @@ -184,6 +191,9 @@ module.exports = { // Burst protection burstProtection, + // Upload rate limiter + uploadPresignLimiter, + // Utility functions createMapsRateLimiter, createUserBasedRateLimiter, diff --git a/backend/package-lock.json b/backend/package-lock.json index 39e1ca7..a7fed21 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -9,8 +9,10 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@aws-sdk/client-s3": "^3.940.0", "@aws-sdk/client-ses": "^3.896.0", "@aws-sdk/credential-providers": "^3.901.0", + "@aws-sdk/s3-request-presigner": "^3.940.0", "@googlemaps/google-maps-services-js": "^3.4.2", "bcryptjs": "^3.0.2", "body-parser": "^2.2.0", @@ -88,6 +90,83 @@ "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", "license": "MIT" }, + "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", @@ -700,6 +779,518 @@ "node": ">=18.0.0" } }, + "node_modules/@aws-sdk/client-s3": { + "version": "3.940.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.940.0.tgz", + "integrity": "sha512-Wi4qnBT6shRRMXuuTgjMFTU5mu2KFWisgcigEMPptjPGUtJvBVi4PTGgS64qsLoUk/obqDAyOBOfEtRZ2ddC2w==", + "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.940.0", + "@aws-sdk/credential-provider-node": "3.940.0", + "@aws-sdk/middleware-bucket-endpoint": "3.936.0", + "@aws-sdk/middleware-expect-continue": "3.936.0", + "@aws-sdk/middleware-flexible-checksums": "3.940.0", + "@aws-sdk/middleware-host-header": "3.936.0", + "@aws-sdk/middleware-location-constraint": "3.936.0", + "@aws-sdk/middleware-logger": "3.936.0", + "@aws-sdk/middleware-recursion-detection": "3.936.0", + "@aws-sdk/middleware-sdk-s3": "3.940.0", + "@aws-sdk/middleware-ssec": "3.936.0", + "@aws-sdk/middleware-user-agent": "3.940.0", + "@aws-sdk/region-config-resolver": "3.936.0", + "@aws-sdk/signature-v4-multi-region": "3.940.0", + "@aws-sdk/types": "3.936.0", + "@aws-sdk/util-endpoints": "3.936.0", + "@aws-sdk/util-user-agent-browser": "3.936.0", + "@aws-sdk/util-user-agent-node": "3.940.0", + "@smithy/config-resolver": "^4.4.3", + "@smithy/core": "^3.18.5", + "@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.12", + "@smithy/middleware-retry": "^4.4.12", + "@smithy/middleware-serde": "^4.2.6", + "@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.8", + "@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.11", + "@smithy/util-defaults-mode-node": "^4.2.14", + "@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", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/client-sso": { + "version": "3.940.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.940.0.tgz", + "integrity": "sha512-SdqJGWVhmIURvCSgkDditHRO+ozubwZk9aCX9MK8qxyOndhobCndW1ozl3hX9psvMAo9Q4bppjuqy/GHWpjB+A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.940.0", + "@aws-sdk/middleware-host-header": "3.936.0", + "@aws-sdk/middleware-logger": "3.936.0", + "@aws-sdk/middleware-recursion-detection": "3.936.0", + "@aws-sdk/middleware-user-agent": "3.940.0", + "@aws-sdk/region-config-resolver": "3.936.0", + "@aws-sdk/types": "3.936.0", + "@aws-sdk/util-endpoints": "3.936.0", + "@aws-sdk/util-user-agent-browser": "3.936.0", + "@aws-sdk/util-user-agent-node": "3.940.0", + "@smithy/config-resolver": "^4.4.3", + "@smithy/core": "^3.18.5", + "@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.12", + "@smithy/middleware-retry": "^4.4.12", + "@smithy/middleware-serde": "^4.2.6", + "@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.8", + "@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.11", + "@smithy/util-defaults-mode-node": "^4.2.14", + "@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.940.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.940.0.tgz", + "integrity": "sha512-KsGD2FLaX5ngJao1mHxodIVU9VYd1E8810fcYiGwO1PFHDzf5BEkp6D9IdMeQwT8Q6JLYtiiT1Y/o3UCScnGoA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.0", + "@aws-sdk/xml-builder": "3.930.0", + "@smithy/core": "^3.18.5", + "@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.8", + "@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.940.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.940.0.tgz", + "integrity": "sha512-/G3l5/wbZYP2XEQiOoIkRJmlv15f1P3MSd1a0gz27lHEMrOJOGq66rF1Ca4OJLzapWt3Fy9BPrZAepoAX11kMw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.940.0", + "@aws-sdk/types": "3.936.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.940.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.940.0.tgz", + "integrity": "sha512-dOrc03DHElNBD6N9Okt4U0zhrG4Wix5QUBSZPr5VN8SvmjD9dkrrxOkkJaMCl/bzrW7kbQEp7LuBdbxArMmOZQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.940.0", + "@aws-sdk/types": "3.936.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.8", + "@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.940.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.940.0.tgz", + "integrity": "sha512-gn7PJQEzb/cnInNFTOaDoCN/hOKqMejNmLof1W5VW95Qk0TPO52lH8R4RmJPnRrwFMswOWswTOpR1roKNLIrcw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.940.0", + "@aws-sdk/credential-provider-env": "3.940.0", + "@aws-sdk/credential-provider-http": "3.940.0", + "@aws-sdk/credential-provider-login": "3.940.0", + "@aws-sdk/credential-provider-process": "3.940.0", + "@aws-sdk/credential-provider-sso": "3.940.0", + "@aws-sdk/credential-provider-web-identity": "3.940.0", + "@aws-sdk/nested-clients": "3.940.0", + "@aws-sdk/types": "3.936.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.940.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.940.0.tgz", + "integrity": "sha512-M8NFAvgvO6xZjiti5kztFiAYmSmSlG3eUfr4ZHSfXYZUA/KUdZU/D6xJyaLnU8cYRWBludb6K9XPKKVwKfqm4g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.940.0", + "@aws-sdk/credential-provider-http": "3.940.0", + "@aws-sdk/credential-provider-ini": "3.940.0", + "@aws-sdk/credential-provider-process": "3.940.0", + "@aws-sdk/credential-provider-sso": "3.940.0", + "@aws-sdk/credential-provider-web-identity": "3.940.0", + "@aws-sdk/types": "3.936.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.940.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.940.0.tgz", + "integrity": "sha512-pILBzt5/TYCqRsJb7vZlxmRIe0/T+FZPeml417EK75060ajDGnVJjHcuVdLVIeKoTKm9gmJc9l45gon6PbHyUQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.940.0", + "@aws-sdk/types": "3.936.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.940.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.940.0.tgz", + "integrity": "sha512-q6JMHIkBlDCOMnA3RAzf8cGfup+8ukhhb50fNpghMs1SNBGhanmaMbZSgLigBRsPQW7fOk2l8jnzdVLS+BB9Uw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.940.0", + "@aws-sdk/core": "3.940.0", + "@aws-sdk/token-providers": "3.940.0", + "@aws-sdk/types": "3.936.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.940.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.940.0.tgz", + "integrity": "sha512-9QLTIkDJHHaYL0nyymO41H8g3ui1yz6Y3GmAN1gYQa6plXisuFBnGAbmKVj7zNvjWaOKdF0dV3dd3AFKEDoJ/w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.940.0", + "@aws-sdk/nested-clients": "3.940.0", + "@aws-sdk/types": "3.936.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.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.936.0.tgz", + "integrity": "sha512-tAaObaAnsP1XnLGndfkGWFuzrJYuk9W0b/nLvol66t8FZExIAf/WdkT2NNAWOYxljVs++oHnyHBCxIlaHrzSiw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.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.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.936.0.tgz", + "integrity": "sha512-aPSJ12d3a3Ea5nyEnLbijCaaYJT2QjQ9iW+zGh5QcZYXmOGWbKVyPSxmVOboZQG+c1M8t6d2O7tqrwzIq8L8qw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.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.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.936.0.tgz", + "integrity": "sha512-l4aGbHpXM45YNgXggIux1HgsCVAvvBoqHPkqLnqMl9QVapfuSTjJHfDYDsx1Xxct6/m7qSMUzanBALhiaGO2fA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.0", + "@aws/lambda-invoke-store": "^0.2.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-user-agent": { + "version": "3.940.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.940.0.tgz", + "integrity": "sha512-nJbLrUj6fY+l2W2rIB9P4Qvpiy0tnTdg/dmixRxrU1z3e8wBdspJlyE+AZN4fuVbeL6rrRrO/zxQC1bB3cw5IA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.940.0", + "@aws-sdk/types": "3.936.0", + "@aws-sdk/util-endpoints": "3.936.0", + "@smithy/core": "^3.18.5", + "@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.940.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.940.0.tgz", + "integrity": "sha512-x0mdv6DkjXqXEcQj3URbCltEzW6hoy/1uIL+i8gExP6YKrnhiZ7SzuB4gPls2UOpK5UqLiqXjhRLfBb1C9i4Dw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.940.0", + "@aws-sdk/middleware-host-header": "3.936.0", + "@aws-sdk/middleware-logger": "3.936.0", + "@aws-sdk/middleware-recursion-detection": "3.936.0", + "@aws-sdk/middleware-user-agent": "3.940.0", + "@aws-sdk/region-config-resolver": "3.936.0", + "@aws-sdk/types": "3.936.0", + "@aws-sdk/util-endpoints": "3.936.0", + "@aws-sdk/util-user-agent-browser": "3.936.0", + "@aws-sdk/util-user-agent-node": "3.940.0", + "@smithy/config-resolver": "^4.4.3", + "@smithy/core": "^3.18.5", + "@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.12", + "@smithy/middleware-retry": "^4.4.12", + "@smithy/middleware-serde": "^4.2.6", + "@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.8", + "@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.11", + "@smithy/util-defaults-mode-node": "^4.2.14", + "@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.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.936.0.tgz", + "integrity": "sha512-wOKhzzWsshXGduxO4pqSiNyL9oUtk4BEvjWm9aaq6Hmfdoydq6v6t0rAGHWPjFwy9z2haovGRi3C8IxdMB4muw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.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/token-providers": { + "version": "3.940.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.940.0.tgz", + "integrity": "sha512-k5qbRe/ZFjW9oWEdzLIa2twRVIEx7p/9rutofyrRysrtEnYh3HAWCngAnwbgKMoiwa806UzcTRx0TjyEpnKcCg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.940.0", + "@aws-sdk/nested-clients": "3.940.0", + "@aws-sdk/types": "3.936.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.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.936.0.tgz", + "integrity": "sha512-uz0/VlMd2pP5MepdrHizd+T+OKfyK4r3OA9JI+L/lPKg0YFQosdJNCKisr6o70E3dh8iMpFYxF1UN/4uZsyARg==", + "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.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.936.0.tgz", + "integrity": "sha512-0Zx3Ntdpu+z9Wlm7JKUBOzS9EunwKAb4KdGUQQxDqh5Lc3ta5uBoub+FgmVuzwnmBu9U1Os8UuwVTH0Lgu+P5w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.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.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.936.0.tgz", + "integrity": "sha512-eZ/XF6NxMtu+iCma58GRNRxSq4lHo6zHQLOZRIeL/ghqYJirqHdenMOwrzPettj60KWlv827RVebP9oNVrwZbw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.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.940.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.940.0.tgz", + "integrity": "sha512-dlD/F+L/jN26I8Zg5x0oDGJiA+/WEQmnSE27fi5ydvYnpfQLwThtQo9SsNS47XSR/SOULaaoC9qx929rZuo74A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.940.0", + "@aws-sdk/types": "3.936.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-s3/node_modules/@aws/lambda-invoke-store": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.1.tgz", + "integrity": "sha512-sIyFcoPZkTtNu9xFeEoynMef3bPJIAbOfUh+ueYcfhVl6xm2VRtMcMclSxmZCMnHHd4hlYKJeq/aggmBEWynww==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@aws-sdk/client-ses": { "version": "3.896.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-ses/-/client-ses-3.896.0.tgz", @@ -914,6 +1505,265 @@ "node": ">=18.0.0" } }, + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.940.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.940.0.tgz", + "integrity": "sha512-fOKC3VZkwa9T2l2VFKWRtfHQPQuISqqNl35ZhcXjWKVwRwl/o7THPMkqI4XwgT2noGa7LLYVbWMwnsgSsBqglg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.940.0", + "@aws-sdk/nested-clients": "3.940.0", + "@aws-sdk/types": "3.936.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/protocol-http": "^5.3.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/credential-provider-login/node_modules/@aws-sdk/core": { + "version": "3.940.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.940.0.tgz", + "integrity": "sha512-KsGD2FLaX5ngJao1mHxodIVU9VYd1E8810fcYiGwO1PFHDzf5BEkp6D9IdMeQwT8Q6JLYtiiT1Y/o3UCScnGoA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.0", + "@aws-sdk/xml-builder": "3.930.0", + "@smithy/core": "^3.18.5", + "@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.8", + "@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/credential-provider-login/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.936.0.tgz", + "integrity": "sha512-tAaObaAnsP1XnLGndfkGWFuzrJYuk9W0b/nLvol66t8FZExIAf/WdkT2NNAWOYxljVs++oHnyHBCxIlaHrzSiw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.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/credential-provider-login/node_modules/@aws-sdk/middleware-logger": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.936.0.tgz", + "integrity": "sha512-aPSJ12d3a3Ea5nyEnLbijCaaYJT2QjQ9iW+zGh5QcZYXmOGWbKVyPSxmVOboZQG+c1M8t6d2O7tqrwzIq8L8qw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.0", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.936.0.tgz", + "integrity": "sha512-l4aGbHpXM45YNgXggIux1HgsCVAvvBoqHPkqLnqMl9QVapfuSTjJHfDYDsx1Xxct6/m7qSMUzanBALhiaGO2fA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.0", + "@aws/lambda-invoke-store": "^0.2.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/credential-provider-login/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.940.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.940.0.tgz", + "integrity": "sha512-nJbLrUj6fY+l2W2rIB9P4Qvpiy0tnTdg/dmixRxrU1z3e8wBdspJlyE+AZN4fuVbeL6rrRrO/zxQC1bB3cw5IA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.940.0", + "@aws-sdk/types": "3.936.0", + "@aws-sdk/util-endpoints": "3.936.0", + "@smithy/core": "^3.18.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/nested-clients": { + "version": "3.940.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.940.0.tgz", + "integrity": "sha512-x0mdv6DkjXqXEcQj3URbCltEzW6hoy/1uIL+i8gExP6YKrnhiZ7SzuB4gPls2UOpK5UqLiqXjhRLfBb1C9i4Dw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.940.0", + "@aws-sdk/middleware-host-header": "3.936.0", + "@aws-sdk/middleware-logger": "3.936.0", + "@aws-sdk/middleware-recursion-detection": "3.936.0", + "@aws-sdk/middleware-user-agent": "3.940.0", + "@aws-sdk/region-config-resolver": "3.936.0", + "@aws-sdk/types": "3.936.0", + "@aws-sdk/util-endpoints": "3.936.0", + "@aws-sdk/util-user-agent-browser": "3.936.0", + "@aws-sdk/util-user-agent-node": "3.940.0", + "@smithy/config-resolver": "^4.4.3", + "@smithy/core": "^3.18.5", + "@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.12", + "@smithy/middleware-retry": "^4.4.12", + "@smithy/middleware-serde": "^4.2.6", + "@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.8", + "@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.11", + "@smithy/util-defaults-mode-node": "^4.2.14", + "@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/credential-provider-login/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.936.0.tgz", + "integrity": "sha512-wOKhzzWsshXGduxO4pqSiNyL9oUtk4BEvjWm9aaq6Hmfdoydq6v6t0rAGHWPjFwy9z2haovGRi3C8IxdMB4muw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.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/credential-provider-login/node_modules/@aws-sdk/types": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.936.0.tgz", + "integrity": "sha512-uz0/VlMd2pP5MepdrHizd+T+OKfyK4r3OA9JI+L/lPKg0YFQosdJNCKisr6o70E3dh8iMpFYxF1UN/4uZsyARg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/util-endpoints": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.936.0.tgz", + "integrity": "sha512-0Zx3Ntdpu+z9Wlm7JKUBOzS9EunwKAb4KdGUQQxDqh5Lc3ta5uBoub+FgmVuzwnmBu9U1Os8UuwVTH0Lgu+P5w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.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/credential-provider-login/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.936.0.tgz", + "integrity": "sha512-eZ/XF6NxMtu+iCma58GRNRxSq4lHo6zHQLOZRIeL/ghqYJirqHdenMOwrzPettj60KWlv827RVebP9oNVrwZbw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.0", + "@smithy/types": "^4.9.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.940.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.940.0.tgz", + "integrity": "sha512-dlD/F+L/jN26I8Zg5x0oDGJiA+/WEQmnSE27fi5ydvYnpfQLwThtQo9SsNS47XSR/SOULaaoC9qx929rZuo74A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.940.0", + "@aws-sdk/types": "3.936.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/credential-provider-login/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/credential-provider-login/node_modules/@aws/lambda-invoke-store": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.1.tgz", + "integrity": "sha512-sIyFcoPZkTtNu9xFeEoynMef3bPJIAbOfUh+ueYcfhVl6xm2VRtMcMclSxmZCMnHHd4hlYKJeq/aggmBEWynww==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@aws-sdk/credential-provider-node": { "version": "3.896.0", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.896.0.tgz", @@ -1458,6 +2308,140 @@ "node": ">=18.0.0" } }, + "node_modules/@aws-sdk/middleware-bucket-endpoint": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.936.0.tgz", + "integrity": "sha512-XLSVVfAorUxZh6dzF+HTOp4R1B5EQcdpGcPliWr0KUj2jukgjZEcqbBmjyMF/p9bmyQsONX80iURF1HLAlW0qg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.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.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.936.0.tgz", + "integrity": "sha512-uz0/VlMd2pP5MepdrHizd+T+OKfyK4r3OA9JI+L/lPKg0YFQosdJNCKisr6o70E3dh8iMpFYxF1UN/4uZsyARg==", + "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.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.936.0.tgz", + "integrity": "sha512-Eb4ELAC23bEQLJmUMYnPWcjD3FZIsmz2svDiXEcxRkQU9r7NRID7pM7C5NPH94wOfiCk0b2Y8rVyFXW0lGQwbA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.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.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.936.0.tgz", + "integrity": "sha512-uz0/VlMd2pP5MepdrHizd+T+OKfyK4r3OA9JI+L/lPKg0YFQosdJNCKisr6o70E3dh8iMpFYxF1UN/4uZsyARg==", + "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.940.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.940.0.tgz", + "integrity": "sha512-WdsxDAVj5qaa5ApAP+JbpCOMHFGSmzjs2Y2OBSbWPeR9Ew7t/Okj+kUub94QJPsgzhvU1/cqNejhsw5VxeFKSQ==", + "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.940.0", + "@aws-sdk/types": "3.936.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.940.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.940.0.tgz", + "integrity": "sha512-KsGD2FLaX5ngJao1mHxodIVU9VYd1E8810fcYiGwO1PFHDzf5BEkp6D9IdMeQwT8Q6JLYtiiT1Y/o3UCScnGoA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.0", + "@aws-sdk/xml-builder": "3.930.0", + "@smithy/core": "^3.18.5", + "@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.8", + "@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.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.936.0.tgz", + "integrity": "sha512-uz0/VlMd2pP5MepdrHizd+T+OKfyK4r3OA9JI+L/lPKg0YFQosdJNCKisr6o70E3dh8iMpFYxF1UN/4uZsyARg==", + "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.893.0", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.893.0.tgz", @@ -1473,6 +2457,33 @@ "node": ">=18.0.0" } }, + "node_modules/@aws-sdk/middleware-location-constraint": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.936.0.tgz", + "integrity": "sha512-SCMPenDtQMd9o5da9JzkHz838w3327iqXk3cbNnXWqnNRx6unyW8FL0DZ84gIY12kAyVHz5WEqlWuekc15ehfw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.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.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.936.0.tgz", + "integrity": "sha512-uz0/VlMd2pP5MepdrHizd+T+OKfyK4r3OA9JI+L/lPKg0YFQosdJNCKisr6o70E3dh8iMpFYxF1UN/4uZsyARg==", + "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.893.0", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.893.0.tgz", @@ -1503,6 +2514,109 @@ "node": ">=18.0.0" } }, + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.940.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.940.0.tgz", + "integrity": "sha512-JYkLjgS1wLoKHJ40G63+afM1ehmsPsjcmrHirKh8+kSCx4ip7+nL1e/twV4Zicxr8RJi9Y0Ahq5mDvneilDDKQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.940.0", + "@aws-sdk/types": "3.936.0", + "@aws-sdk/util-arn-parser": "3.893.0", + "@smithy/core": "^3.18.5", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/protocol-http": "^5.3.5", + "@smithy/signature-v4": "^5.3.5", + "@smithy/smithy-client": "^4.9.8", + "@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/middleware-sdk-s3/node_modules/@aws-sdk/core": { + "version": "3.940.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.940.0.tgz", + "integrity": "sha512-KsGD2FLaX5ngJao1mHxodIVU9VYd1E8810fcYiGwO1PFHDzf5BEkp6D9IdMeQwT8Q6JLYtiiT1Y/o3UCScnGoA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.0", + "@aws-sdk/xml-builder": "3.930.0", + "@smithy/core": "^3.18.5", + "@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.8", + "@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-sdk-s3/node_modules/@aws-sdk/types": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.936.0.tgz", + "integrity": "sha512-uz0/VlMd2pP5MepdrHizd+T+OKfyK4r3OA9JI+L/lPKg0YFQosdJNCKisr6o70E3dh8iMpFYxF1UN/4uZsyARg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-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/middleware-ssec": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.936.0.tgz", + "integrity": "sha512-/GLC9lZdVp05ozRik5KsuODR/N7j+W+2TbfdFL3iS+7un+gnP6hC8RDOZd6WhpZp7drXQ9guKiTAxkZQwzS8DA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.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.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.936.0.tgz", + "integrity": "sha512-uz0/VlMd2pP5MepdrHizd+T+OKfyK4r3OA9JI+L/lPKg0YFQosdJNCKisr6o70E3dh8iMpFYxF1UN/4uZsyARg==", + "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.896.0", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.896.0.tgz", @@ -1587,6 +2701,68 @@ "node": ">=18.0.0" } }, + "node_modules/@aws-sdk/s3-request-presigner": { + "version": "3.940.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.940.0.tgz", + "integrity": "sha512-TgTUDM2H7revReDfkVwVtIqxV3K0cJLdyuLDIkefVHRUNKwU1Vd5FB2TaFrs6STO0kx5pTckDCOLh0iy7nW5WQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/signature-v4-multi-region": "3.940.0", + "@aws-sdk/types": "3.936.0", + "@aws-sdk/util-format-url": "3.936.0", + "@smithy/middleware-endpoint": "^4.3.12", + "@smithy/protocol-http": "^5.3.5", + "@smithy/smithy-client": "^4.9.8", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/s3-request-presigner/node_modules/@aws-sdk/types": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.936.0.tgz", + "integrity": "sha512-uz0/VlMd2pP5MepdrHizd+T+OKfyK4r3OA9JI+L/lPKg0YFQosdJNCKisr6o70E3dh8iMpFYxF1UN/4uZsyARg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.940.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.940.0.tgz", + "integrity": "sha512-ugHZEoktD/bG6mdgmhzLDjMP2VrYRAUPRPF1DpCyiZexkH7DCU7XrSJyXMvkcf0DHV+URk0q2sLf/oqn1D2uYw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-sdk-s3": "3.940.0", + "@aws-sdk/types": "3.936.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/signature-v4-multi-region/node_modules/@aws-sdk/types": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.936.0.tgz", + "integrity": "sha512-uz0/VlMd2pP5MepdrHizd+T+OKfyK4r3OA9JI+L/lPKg0YFQosdJNCKisr6o70E3dh8iMpFYxF1UN/4uZsyARg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@aws-sdk/token-providers": { "version": "3.896.0", "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.896.0.tgz", @@ -1618,6 +2794,18 @@ "node": ">=18.0.0" } }, + "node_modules/@aws-sdk/util-arn-parser": { + "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==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@aws-sdk/util-endpoints": { "version": "3.895.0", "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.895.0.tgz", @@ -1634,6 +2822,34 @@ "node": ">=18.0.0" } }, + "node_modules/@aws-sdk/util-format-url": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.936.0.tgz", + "integrity": "sha512-MS5eSEtDUFIAMHrJaMERiHAvDPdfxc/T869ZjDNFAIiZhyc037REw0aoTNeimNXDNy2txRNZJaAUn/kE4RwN+g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.936.0", + "@smithy/querystring-builder": "^4.2.5", + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-format-url/node_modules/@aws-sdk/types": { + "version": "3.936.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.936.0.tgz", + "integrity": "sha512-uz0/VlMd2pP5MepdrHizd+T+OKfyK4r3OA9JI+L/lPKg0YFQosdJNCKisr6o70E3dh8iMpFYxF1UN/4uZsyARg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.9.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@aws-sdk/util-locate-window": { "version": "3.893.0", "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.893.0.tgz", @@ -2939,12 +4155,37 @@ } }, "node_modules/@smithy/abort-controller": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.0.tgz", - "integrity": "sha512-PLUYa+SUKOEZtXFURBu/CNxlsxfaFGxSBPcStL13KpVeVWIfdezWyDqkz7iDLmwnxojXD0s5KzuB5HGHvt4Aeg==", + "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.6.0", + "@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": { + "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": { @@ -2952,15 +4193,16 @@ } }, "node_modules/@smithy/config-resolver": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.3.0.tgz", - "integrity": "sha512-9oH+n8AVNiLPK/iK/agOsoWfrKZ3FGP3502tkksd6SRsKMYiu7AFX0YXo6YBADdsAj7C+G/aLKdsafIJHxuCkQ==", + "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.0", - "@smithy/types": "^4.6.0", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/types": "^4.9.0", "@smithy/util-config-provider": "^4.2.0", - "@smithy/util-middleware": "^4.2.0", + "@smithy/util-endpoints": "^3.2.5", + "@smithy/util-middleware": "^4.2.5", "tslib": "^2.6.2" }, "engines": { @@ -2968,18 +4210,18 @@ } }, "node_modules/@smithy/core": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.14.0.tgz", - "integrity": "sha512-XJ4z5FxvY/t0Dibms/+gLJrI5niRoY0BCmE02fwmPcRYFPI4KI876xaE79YGWIKnEslMbuQPsIEsoU/DXa0DoA==", + "version": "3.18.5", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.18.5.tgz", + "integrity": "sha512-6gnIz3h+PEPQGDj8MnRSjDvKBah042jEoPgjFGJ4iJLBE78L4lY/n98x14XyPF4u3lN179Ub/ZKFY5za9GeLQw==", "license": "Apache-2.0", "dependencies": { - "@smithy/middleware-serde": "^4.2.0", - "@smithy/protocol-http": "^5.3.0", - "@smithy/types": "^4.6.0", - "@smithy/util-base64": "^4.2.0", + "@smithy/middleware-serde": "^4.2.6", + "@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.0", - "@smithy/util-stream": "^4.4.0", + "@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" @@ -2989,15 +4231,85 @@ } }, "node_modules/@smithy/credential-provider-imds": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.0.tgz", - "integrity": "sha512-SOhFVvFH4D5HJZytb0bLKxCrSnwcqPiNlrw+S4ZXjMnsC+o9JcUQzbZOEQcA8yv9wJFNhfsUiIUKiEnYL68Big==", + "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.0", - "@smithy/property-provider": "^4.2.0", - "@smithy/types": "^4.6.0", - "@smithy/url-parser": "^4.2.0", + "@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": { @@ -3005,15 +4317,30 @@ } }, "node_modules/@smithy/fetch-http-handler": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.0.tgz", - "integrity": "sha512-BG3KSmsx9A//KyIfw+sqNmWFr1YBUr+TwpxFT7yPqAk0yyDh7oSNgzfNH7pS6OC099EGx2ltOULvumCFe8bcgw==", + "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.0", - "@smithy/querystring-builder": "^4.2.0", - "@smithy/types": "^4.6.0", - "@smithy/util-base64": "^4.2.0", + "@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" + }, + "engines": { + "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": { @@ -3021,12 +4348,12 @@ } }, "node_modules/@smithy/hash-node": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.0.tgz", - "integrity": "sha512-ugv93gOhZGysTctZh9qdgng8B+xO0cj+zN0qAZ+Sgh7qTQGPOJbMdIuyP89KNfUyfAqFSNh5tMvC+h2uCpmTtA==", + "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.6.0", + "@smithy/types": "^4.9.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" @@ -3035,13 +4362,27 @@ "node": ">=18.0.0" } }, - "node_modules/@smithy/invalid-dependency": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.0.tgz", - "integrity": "sha512-ZmK5X5fUPAbtvRcUPtk28aqIClVhbfcmfoS4M7UQBTnDdrNxhsrxYVv0ZEl5NaPSyExsPWqL4GsPlRvtlwg+2A==", + "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.6.0", + "@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.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.9.0", "tslib": "^2.6.2" }, "engines": { @@ -3060,14 +4401,28 @@ "node": ">=18.0.0" } }, - "node_modules/@smithy/middleware-content-length": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.0.tgz", - "integrity": "sha512-6ZAnwrXFecrA4kIDOcz6aLBhU5ih2is2NdcZtobBDSdSHtE9a+MThB5uqyK4XXesdOCvOcbCm2IGB95birTSOQ==", + "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/protocol-http": "^5.3.0", - "@smithy/types": "^4.6.0", + "@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.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.5", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -3075,18 +4430,18 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.3.0.tgz", - "integrity": "sha512-jFVjuQeV8TkxaRlcCNg0GFVgg98tscsmIrIwRFeC74TIUyLE3jmY9xgc1WXrPQYRjQNK3aRoaIk6fhFRGOIoGw==", + "version": "4.3.12", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.3.12.tgz", + "integrity": "sha512-9pAX/H+VQPzNbouhDhkW723igBMLgrI8OtX+++M7iKJgg/zY/Ig3i1e6seCcx22FWhE6Q/S61BRdi2wXBORT+A==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.14.0", - "@smithy/middleware-serde": "^4.2.0", - "@smithy/node-config-provider": "^4.3.0", - "@smithy/shared-ini-file-loader": "^4.3.0", - "@smithy/types": "^4.6.0", - "@smithy/url-parser": "^4.2.0", - "@smithy/util-middleware": "^4.2.0", + "@smithy/core": "^3.18.5", + "@smithy/middleware-serde": "^4.2.6", + "@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": { @@ -3094,18 +4449,18 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.0.tgz", - "integrity": "sha512-yaVBR0vQnOnzex45zZ8ZrPzUnX73eUC8kVFaAAbn04+6V7lPtxn56vZEBBAhgS/eqD6Zm86o6sJs6FuQVoX5qg==", + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.12.tgz", + "integrity": "sha512-S4kWNKFowYd0lID7/DBqWHOQxmxlsf0jBaos9chQZUWTVOjSW1Ogyh8/ib5tM+agFDJ/TCxuCTvrnlc+9cIBcQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.0", - "@smithy/protocol-http": "^5.3.0", - "@smithy/service-error-classification": "^4.2.0", - "@smithy/smithy-client": "^4.7.0", - "@smithy/types": "^4.6.0", - "@smithy/util-middleware": "^4.2.0", - "@smithy/util-retry": "^4.2.0", + "@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.8", + "@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" }, @@ -3114,13 +4469,13 @@ } }, "node_modules/@smithy/middleware-serde": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.0.tgz", - "integrity": "sha512-rpTQ7D65/EAbC6VydXlxjvbifTf4IH+sADKg6JmAvhkflJO2NvDeyU9qsWUNBelJiQFcXKejUHWRSdmpJmEmiw==", + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.6.tgz", + "integrity": "sha512-VkLoE/z7e2g8pirwisLz8XJWedUSY8my/qrp81VmAdyrhi94T+riBfwP+AOEEFR9rFTSonC/5D2eWNmFabHyGQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.0", - "@smithy/types": "^4.6.0", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -3128,12 +4483,12 @@ } }, "node_modules/@smithy/middleware-stack": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.0.tgz", - "integrity": "sha512-G5CJ//eqRd9OARrQu9MK1H8fNm2sMtqFh6j8/rPozhEL+Dokpvi1Og+aCixTuwDAGZUkJPk6hJT5jchbk/WCyg==", + "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.6.0", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -3141,14 +4496,14 @@ } }, "node_modules/@smithy/node-config-provider": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.0.tgz", - "integrity": "sha512-5QgHNuWdT9j9GwMPPJCKxy2KDxZ3E5l4M3/5TatSZrqYVoEiqQrDfAq8I6KWZw7RZOHtVtCzEPdYz7rHZixwcA==", + "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.0", - "@smithy/shared-ini-file-loader": "^4.3.0", - "@smithy/types": "^4.6.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": { @@ -3156,15 +4511,15 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.3.0.tgz", - "integrity": "sha512-RHZ/uWCmSNZ8cneoWEVsVwMZBKy/8123hEpm57vgGXA3Irf/Ja4v9TVshHK2ML5/IqzAZn0WhINHOP9xl+Qy6Q==", + "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.0", - "@smithy/protocol-http": "^5.3.0", - "@smithy/querystring-builder": "^4.2.0", - "@smithy/types": "^4.6.0", + "@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": { @@ -3172,12 +4527,12 @@ } }, "node_modules/@smithy/property-provider": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.0.tgz", - "integrity": "sha512-rV6wFre0BU6n/tx2Ztn5LdvEdNZ2FasQbPQmDOPfV9QQyDmsCkOAB0osQjotRCQg+nSKFmINhyda0D3AnjSBJw==", + "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.6.0", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -3185,12 +4540,12 @@ } }, "node_modules/@smithy/protocol-http": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.0.tgz", - "integrity": "sha512-6POSYlmDnsLKb7r1D3SVm7RaYW6H1vcNcTWGWrF7s9+2noNYvUsm7E4tz5ZQ9HXPmKn6Hb67pBDRIjrT4w/d7Q==", + "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.6.0", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -3198,12 +4553,12 @@ } }, "node_modules/@smithy/querystring-builder": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.0.tgz", - "integrity": "sha512-Q4oFD0ZmI8yJkiPPeGUITZj++4HHYCW3pYBYfIobUCkYpI6mbkzmG1MAQQ3lJYYWj3iNqfzOenUZu+jqdPQ16A==", + "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.6.0", + "@smithy/types": "^4.9.0", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" }, @@ -3212,12 +4567,12 @@ } }, "node_modules/@smithy/querystring-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.0.tgz", - "integrity": "sha512-BjATSNNyvVbQxOOlKse0b0pSezTWGMvA87SvoFoFlkRsKXVsN3bEtjCxvsNXJXfnAzlWFPaT9DmhWy1vn0sNEA==", + "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.6.0", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -3225,24 +4580,24 @@ } }, "node_modules/@smithy/service-error-classification": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.0.tgz", - "integrity": "sha512-Ylv1ttUeKatpR0wEOMnHf1hXMktPUMObDClSWl2TpCVT4DwtJhCeighLzSLbgH3jr5pBNM0LDXT5yYxUvZ9WpA==", + "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.6.0" + "@smithy/types": "^4.9.0" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/shared-ini-file-loader": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.3.0.tgz", - "integrity": "sha512-VCUPPtNs+rKWlqqntX0CbVvWyjhmX30JCtzO+s5dlzzxrvSfRh5SY0yxnkirvc1c80vdKQttahL71a9EsdolSQ==", + "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.6.0", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -3250,16 +4605,16 @@ } }, "node_modules/@smithy/signature-v4": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.0.tgz", - "integrity": "sha512-MKNyhXEs99xAZaFhm88h+3/V+tCRDQ+PrDzRqL0xdDpq4gjxcMmf5rBA3YXgqZqMZ/XwemZEurCBQMfxZOWq/g==", + "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.0", - "@smithy/types": "^4.6.0", + "@smithy/protocol-http": "^5.3.5", + "@smithy/types": "^4.9.0", "@smithy/util-hex-encoding": "^4.2.0", - "@smithy/util-middleware": "^4.2.0", + "@smithy/util-middleware": "^4.2.5", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" @@ -3269,17 +4624,17 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.7.0.tgz", - "integrity": "sha512-3BDx/aCCPf+kkinYf5QQhdQ9UAGihgOVqI3QO5xQfSaIWvUE4KYLtiGRWsNe1SR7ijXC0QEPqofVp5Sb0zC8xQ==", + "version": "4.9.8", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.9.8.tgz", + "integrity": "sha512-8xgq3LgKDEFoIrLWBho/oYKyWByw9/corz7vuh1upv7ZBm0ZMjGYBhbn6v643WoIqA9UTcx5A5htEp/YatUwMA==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.14.0", - "@smithy/middleware-endpoint": "^4.3.0", - "@smithy/middleware-stack": "^4.2.0", - "@smithy/protocol-http": "^5.3.0", - "@smithy/types": "^4.6.0", - "@smithy/util-stream": "^4.4.0", + "@smithy/core": "^3.18.5", + "@smithy/middleware-endpoint": "^4.3.12", + "@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": { @@ -3287,9 +4642,9 @@ } }, "node_modules/@smithy/types": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.6.0.tgz", - "integrity": "sha512-4lI9C8NzRPOv66FaY1LL1O/0v0aLVrq/mXP/keUa9mJOApEeae43LsLd2kZRUJw91gxOQfLIrV3OvqPgWz1YsA==", + "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" @@ -3299,13 +4654,13 @@ } }, "node_modules/@smithy/url-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.0.tgz", - "integrity": "sha512-AlBmD6Idav2ugmoAL6UtR6ItS7jU5h5RNqLMZC7QrLCoITA9NzIN3nx9GWi8g4z1pfWh2r9r96SX/jHiNwPJ9A==", + "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.0", - "@smithy/types": "^4.6.0", + "@smithy/querystring-parser": "^4.2.5", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -3313,9 +4668,9 @@ } }, "node_modules/@smithy/util-base64": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.2.0.tgz", - "integrity": "sha512-+erInz8WDv5KPe7xCsJCp+1WCjSbah9gWcmUXc9NqmhyPx59tf7jqFz+za1tRG1Y5KM1Cy1rWCcGypylFp4mvA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.0.tgz", + "integrity": "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ==", "license": "Apache-2.0", "dependencies": { "@smithy/util-buffer-from": "^4.2.0", @@ -3339,9 +4694,9 @@ } }, "node_modules/@smithy/util-body-length-node": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.0.tgz", - "integrity": "sha512-U8q1WsSZFjXijlD7a4wsDQOvOwV+72iHSfq1q7VD+V75xP/pdtm0WIGuaFJ3gcADDOKj2MIBn4+zisi140HEnQ==", + "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==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -3376,15 +4731,14 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.2.0.tgz", - "integrity": "sha512-qzHp7ZDk1Ba4LDwQVCNp90xPGqSu7kmL7y5toBpccuhi3AH7dcVBIT/pUxYcInK4jOy6FikrcTGq5wxcka8UaQ==", + "version": "4.3.11", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.11.tgz", + "integrity": "sha512-yHv+r6wSQXEXTPVCIQTNmXVWs7ekBTpMVErjqZoWkYN75HIFN5y9+/+sYOejfAuvxWGvgzgxbTHa/oz61YTbKw==", "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.2.0", - "@smithy/smithy-client": "^4.7.0", - "@smithy/types": "^4.6.0", - "bowser": "^2.11.0", + "@smithy/property-provider": "^4.2.5", + "@smithy/smithy-client": "^4.9.8", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -3392,17 +4746,17 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.0.tgz", - "integrity": "sha512-FxUHS3WXgx3bTWR6yQHNHHkQHZm/XKIi/CchTnKvBulN6obWpcbzJ6lDToXn+Wp0QlVKd7uYAz2/CTw1j7m+Kg==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.14.tgz", + "integrity": "sha512-ljZN3iRvaJUgulfvobIuG97q1iUuCMrvXAlkZ4msY+ZuVHQHDIqn7FKZCEj+bx8omz6kF5yQXms/xhzjIO5XiA==", "license": "Apache-2.0", "dependencies": { - "@smithy/config-resolver": "^4.3.0", - "@smithy/credential-provider-imds": "^4.2.0", - "@smithy/node-config-provider": "^4.3.0", - "@smithy/property-provider": "^4.2.0", - "@smithy/smithy-client": "^4.7.0", - "@smithy/types": "^4.6.0", + "@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.8", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -3410,13 +4764,13 @@ } }, "node_modules/@smithy/util-endpoints": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.2.0.tgz", - "integrity": "sha512-TXeCn22D56vvWr/5xPqALc9oO+LN+QpFjrSM7peG/ckqEPoI3zaKZFp+bFwfmiHhn5MGWPaLCqDOJPPIixk9Wg==", + "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.0", - "@smithy/types": "^4.6.0", + "@smithy/node-config-provider": "^4.3.5", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -3436,12 +4790,12 @@ } }, "node_modules/@smithy/util-middleware": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.0.tgz", - "integrity": "sha512-u9OOfDa43MjagtJZ8AapJcmimP+K2Z7szXn8xbty4aza+7P1wjFmy2ewjSbhEiYQoW1unTlOAIV165weYAaowA==", + "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.6.0", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -3449,13 +4803,13 @@ } }, "node_modules/@smithy/util-retry": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.0.tgz", - "integrity": "sha512-BWSiuGbwRnEE2SFfaAZEX0TqaxtvtSYPM/J73PFVm+A29Fg1HTPiYFb8TmX1DXp4hgcdyJcNQmprfd5foeORsg==", + "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.0", - "@smithy/types": "^4.6.0", + "@smithy/service-error-classification": "^4.2.5", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { @@ -3463,15 +4817,15 @@ } }, "node_modules/@smithy/util-stream": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.4.0.tgz", - "integrity": "sha512-vtO7ktbixEcrVzMRmpQDnw/Ehr9UWjBvSJ9fyAbadKkC4w5Cm/4lMO8cHz8Ysb8uflvQUNRcuux/oNHKPXkffg==", + "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.0", - "@smithy/node-http-handler": "^4.3.0", - "@smithy/types": "^4.6.0", - "@smithy/util-base64": "^4.2.0", + "@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", "@smithy/util-utf8": "^4.2.0", @@ -3507,13 +4861,13 @@ } }, "node_modules/@smithy/util-waiter": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.1.1.tgz", - "integrity": "sha512-PJBmyayrlfxM7nbqjomF4YcT1sApQwZio0NHSsT0EzhJqljRmvhzqZua43TyEs80nJk2Cn2FGPg/N8phH6KeCQ==", + "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.1.1", - "@smithy/types": "^4.5.0", + "@smithy/abort-controller": "^4.2.5", + "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "engines": { diff --git a/backend/package.json b/backend/package.json index e22fad8..4296e94 100644 --- a/backend/package.json +++ b/backend/package.json @@ -34,8 +34,10 @@ "author": "", "license": "ISC", "dependencies": { + "@aws-sdk/client-s3": "^3.940.0", "@aws-sdk/client-ses": "^3.896.0", "@aws-sdk/credential-providers": "^3.901.0", + "@aws-sdk/s3-request-presigner": "^3.940.0", "@googlemaps/google-maps-services-js": "^3.4.2", "bcryptjs": "^3.0.2", "body-parser": "^2.2.0", diff --git a/backend/routes/conditionChecks.js b/backend/routes/conditionChecks.js index cb1812f..9800655 100644 --- a/backend/routes/conditionChecks.js +++ b/backend/routes/conditionChecks.js @@ -1,77 +1,56 @@ const express = require("express"); -const multer = require("multer"); const { authenticateToken } = require("../middleware/auth"); const ConditionCheckService = require("../services/conditionCheckService"); const logger = require("../utils/logger"); const router = express.Router(); -// Configure multer for photo uploads -const upload = multer({ - dest: "uploads/condition-checks/", - limits: { - fileSize: 10 * 1024 * 1024, // 10MB limit - files: 20, // Maximum 20 files - }, - fileFilter: (req, file, cb) => { - // Accept only image files - if (file.mimetype.startsWith("image/")) { - cb(null, true); - } else { - cb(new Error("Only image files are allowed"), false); - } - }, -}); - // Submit a condition check -router.post( - "/:rentalId", - authenticateToken, - upload.array("imageFilenames"), - async (req, res) => { - try { - const { rentalId } = req.params; - const { checkType, notes } = req.body; - const userId = req.user.id; +router.post("/:rentalId", authenticateToken, async (req, res) => { + try { + const { rentalId } = req.params; + const { checkType, notes, imageFilenames: rawImageFilenames } = req.body; + const userId = req.user.id; - // Get uploaded file paths - const imageFilenames = req.files ? req.files.map((file) => file.path) : []; + // Ensure imageFilenames is an array (S3 keys) + const imageFilenames = Array.isArray(rawImageFilenames) + ? rawImageFilenames + : []; - const conditionCheck = await ConditionCheckService.submitConditionCheck( - rentalId, - checkType, - userId, - imageFilenames, - notes - ); + const conditionCheck = await ConditionCheckService.submitConditionCheck( + rentalId, + checkType, + userId, + imageFilenames, + notes + ); - const reqLogger = logger.withRequestId(req.id); - reqLogger.info("Condition check submitted", { - rentalId, - checkType, - userId, - photoCount: imageFilenames.length, - }); + const reqLogger = logger.withRequestId(req.id); + reqLogger.info("Condition check submitted", { + rentalId, + checkType, + userId, + photoCount: imageFilenames.length, + }); - res.status(201).json({ - success: true, - conditionCheck, - }); - } catch (error) { - const reqLogger = logger.withRequestId(req.id); - reqLogger.error("Error submitting condition check", { - error: error.message, - rentalId: req.params.rentalId, - userId: req.user?.id, - }); + res.status(201).json({ + success: true, + conditionCheck, + }); + } catch (error) { + const reqLogger = logger.withRequestId(req.id); + reqLogger.error("Error submitting condition check", { + error: error.message, + rentalId: req.params.rentalId, + userId: req.user?.id, + }); - res.status(400).json({ - success: false, - error: error.message, - }); - } + res.status(400).json({ + success: false, + error: error.message, + }); } -); +}); // Get condition checks for a rental router.get("/:rentalId", authenticateToken, async (req, res) => { diff --git a/backend/routes/forum.js b/backend/routes/forum.js index dcb2a71..e59a1ab 100644 --- a/backend/routes/forum.js +++ b/backend/routes/forum.js @@ -2,7 +2,6 @@ const express = require('express'); const { Op } = require('sequelize'); const { ForumPost, ForumComment, PostTag, User } = require('../models'); const { authenticateToken, requireAdmin, optionalAuth } = require('../middleware/auth'); -const { uploadForumPostImages, uploadForumCommentImages } = require('../middleware/upload'); const logger = require('../utils/logger'); const emailServices = require('../services/email'); const googleMapsService = require('../services/googleMapsService'); @@ -238,21 +237,12 @@ router.get('/posts/:id', optionalAuth, async (req, res, next) => { }); // POST /api/forum/posts - Create new post -router.post('/posts', authenticateToken, uploadForumPostImages, async (req, res, next) => { +router.post('/posts', authenticateToken, async (req, res, next) => { try { - let { title, content, category, tags, zipCode, latitude: providedLat, longitude: providedLng } = req.body; + let { title, content, category, tags, zipCode, latitude: providedLat, longitude: providedLng, imageFilenames } = req.body; - // Parse tags if they come as JSON string (from FormData) - if (typeof tags === 'string') { - try { - tags = JSON.parse(tags); - } catch (e) { - tags = []; - } - } - - // Extract image filenames if uploaded - const imageFilenames = req.files ? req.files.map(file => file.filename) : []; + // Ensure imageFilenames is an array + imageFilenames = Array.isArray(imageFilenames) ? imageFilenames : []; // Initialize location fields let latitude = null; @@ -913,9 +903,11 @@ router.patch('/posts/:id/accept-answer', authenticateToken, async (req, res, nex }); // POST /api/forum/posts/:id/comments - Add comment/reply -router.post('/posts/:id/comments', authenticateToken, uploadForumCommentImages, async (req, res, next) => { +router.post('/posts/:id/comments', authenticateToken, async (req, res, next) => { try { - const { content, parentCommentId } = req.body; + // Support both parentId (new) and parentCommentId (legacy) for backwards compatibility + const { content, parentId, parentCommentId, imageFilenames: rawImageFilenames } = req.body; + const parentIdResolved = parentId || parentCommentId; const post = await ForumPost.findByPk(req.params.id); if (!post) { @@ -928,21 +920,21 @@ router.post('/posts/:id/comments', authenticateToken, uploadForumCommentImages, } // Validate parent comment if provided - if (parentCommentId) { - const parentComment = await ForumComment.findByPk(parentCommentId); + if (parentIdResolved) { + const parentComment = await ForumComment.findByPk(parentIdResolved); if (!parentComment || parentComment.postId !== post.id) { return res.status(400).json({ error: 'Invalid parent comment' }); } } - // Extract image filenames if uploaded - const imageFilenames = req.files ? req.files.map(file => file.filename) : []; + // Ensure imageFilenames is an array + const imageFilenames = Array.isArray(rawImageFilenames) ? rawImageFilenames : []; const comment = await ForumComment.create({ postId: req.params.id, authorId: req.user.id, content, - parentCommentId: parentCommentId || null, + parentCommentId: parentIdResolved || null, imageFilenames }); diff --git a/backend/routes/messages.js b/backend/routes/messages.js index 9dec864..544aec9 100644 --- a/backend/routes/messages.js +++ b/backend/routes/messages.js @@ -2,7 +2,6 @@ const express = require('express'); const helmet = require('helmet'); const { Message, User } = require('../models'); const { authenticateToken } = require('../middleware/auth'); -const { uploadMessageImage } = require('../middleware/upload'); const logger = require('../utils/logger'); const { emitNewMessage, emitMessageRead } = require('../sockets/messageSocket'); const { Op } = require('sequelize'); @@ -237,9 +236,9 @@ router.get('/:id', authenticateToken, async (req, res, next) => { }); // Send a new message -router.post('/', authenticateToken, uploadMessageImage, async (req, res, next) => { +router.post('/', authenticateToken, async (req, res, next) => { try { - const { receiverId, content } = req.body; + const { receiverId, content, imageFilename } = req.body; // Check if receiver exists const receiver = await User.findByPk(receiverId); @@ -252,14 +251,11 @@ router.post('/', authenticateToken, uploadMessageImage, async (req, res, next) = return res.status(400).json({ error: 'Cannot send messages to yourself' }); } - // Extract image filename if uploaded - const imageFilename = req.file ? req.file.filename : null; - const message = await Message.create({ senderId: req.user.id, receiverId, content, - imageFilename + imageFilename: imageFilename || null }); const messageWithSender = await Message.findByPk(message.id, { diff --git a/backend/routes/upload.js b/backend/routes/upload.js new file mode 100644 index 0000000..d9497a3 --- /dev/null +++ b/backend/routes/upload.js @@ -0,0 +1,214 @@ +const express = require("express"); +const router = express.Router(); +const { authenticateToken } = require("../middleware/auth"); +const { uploadPresignLimiter } = require("../middleware/rateLimiter"); +const s3Service = require("../services/s3Service"); +const S3OwnershipService = require("../services/s3OwnershipService"); +const logger = require("../utils/logger"); +const MAX_BATCH_SIZE = 20; + +/** + * Middleware to check if S3 is enabled + */ +const requireS3Enabled = (req, res, next) => { + if (!s3Service.isEnabled()) { + return res.status(503).json({ + error: "File upload service is not available", + }); + } + next(); +}; + +/** + * POST /api/upload/presign + * Get a presigned URL for uploading a single file to S3 + */ +router.post( + "/presign", + authenticateToken, + requireS3Enabled, + uploadPresignLimiter, + async (req, res, next) => { + try { + const { uploadType, contentType, fileName, fileSize } = req.body; + + // Validate required fields + if (!uploadType || !contentType || !fileName || !fileSize) { + return res.status(400).json({ error: "Missing required fields" }); + } + + const result = await s3Service.getPresignedUploadUrl( + uploadType, + contentType, + fileName, + fileSize + ); + + logger.info("Presigned URL generated", { + userId: req.user.id, + uploadType, + key: result.key, + }); + + res.json(result); + } catch (error) { + if (error.message.includes("Invalid")) { + return res.status(400).json({ error: error.message }); + } + next(error); + } + } +); + +/** + * POST /api/upload/presign-batch + * Get presigned URLs for uploading multiple files to S3 + */ +router.post( + "/presign-batch", + authenticateToken, + requireS3Enabled, + uploadPresignLimiter, + async (req, res, next) => { + try { + const { uploadType, files } = req.body; + + if (!uploadType || !files || !Array.isArray(files)) { + return res.status(400).json({ error: "Missing required fields" }); + } + + if (files.length === 0) { + return res.status(400).json({ error: "No files specified" }); + } + + if (files.length > MAX_BATCH_SIZE) { + return res + .status(400) + .json({ error: "Maximum ${MAX_BATCH_SIZE} files per batch" }); + } + + // Validate each file has required fields + for (const file of files) { + if (!file.contentType || !file.fileName || !file.fileSize) { + return res.status(400).json({ + error: "Each file must have contentType, fileName, and fileSize", + }); + } + } + + const results = await Promise.all( + files.map((f) => + s3Service.getPresignedUploadUrl( + uploadType, + f.contentType, + f.fileName, + f.fileSize + ) + ) + ); + + logger.info("Batch presigned URLs generated", { + userId: req.user.id, + uploadType, + count: results.length, + }); + + res.json({ uploads: results }); + } catch (error) { + if (error.message.includes("Invalid")) { + return res.status(400).json({ error: error.message }); + } + next(error); + } + } +); + +/** + * POST /api/upload/confirm + * Confirm that files have been uploaded to S3 + */ +router.post( + "/confirm", + authenticateToken, + requireS3Enabled, + async (req, res, next) => { + try { + const { keys } = req.body; + + if (!keys || !Array.isArray(keys)) { + return res.status(400).json({ error: "Missing keys array" }); + } + + if (keys.length === 0) { + return res.status(400).json({ error: "No keys specified" }); + } + + const results = await Promise.all( + keys.map(async (key) => ({ + key, + exists: await s3Service.verifyUpload(key), + })) + ); + + const confirmed = results.filter((r) => r.exists).map((r) => r.key); + + logger.info("Upload confirmation", { + userId: req.user.id, + confirmed: confirmed.length, + total: keys.length, + }); + + // Only return confirmed keys, not which ones failed (prevents file existence probing) + res.json({ confirmed, total: keys.length }); + } catch (error) { + next(error); + } + } +); + +/** + * GET /api/upload/signed-url/*key + * Get a signed URL for accessing private content (messages, condition-checks) + * The key is the full path after /signed-url/ (e.g., "messages/uuid.jpg") + */ +router.get( + "/signed-url/*key", + authenticateToken, + requireS3Enabled, + async (req, res, next) => { + try { + const { key } = req.params; + + // Only allow private folders to use signed URLs + const isPrivate = + key.startsWith("messages/") || key.startsWith("condition-checks/"); + if (!isPrivate) { + return res + .status(400) + .json({ error: "Signed URLs only for private content" }); + } + + // Verify user is authorized to access this file + const authResult = await S3OwnershipService.canAccessFile( + key, + req.user.id + ); + if (!authResult.authorized) { + logger.warn("Unauthorized signed URL request", { + userId: req.user.id, + key, + reason: authResult.reason, + }); + return res.status(403).json({ error: "Access denied" }); + } + + const url = await s3Service.getPresignedDownloadUrl(key); + + res.json({ url, expiresIn: 3600 }); + } catch (error) { + next(error); + } + } +); + +module.exports = router; diff --git a/backend/server.js b/backend/server.js index 7a7a813..ed7afee 100644 --- a/backend/server.js +++ b/backend/server.js @@ -28,11 +28,13 @@ const stripeRoutes = require("./routes/stripe"); const mapsRoutes = require("./routes/maps"); const conditionCheckRoutes = require("./routes/conditionChecks"); const feedbackRoutes = require("./routes/feedback"); +const uploadRoutes = require("./routes/upload"); const PayoutProcessor = require("./jobs/payoutProcessor"); const RentalStatusJob = require("./jobs/rentalStatusJob"); const ConditionCheckReminderJob = require("./jobs/conditionCheckReminder"); const emailServices = require("./services/email"); +const s3Service = require("./services/s3Service"); // Socket.io setup const { authenticateSocket } = require("./sockets/socketAuth"); @@ -159,6 +161,7 @@ app.use("/api/stripe", requireAlphaAccess, stripeRoutes); app.use("/api/maps", requireAlphaAccess, mapsRoutes); app.use("/api/condition-checks", requireAlphaAccess, conditionCheckRoutes); app.use("/api/feedback", requireAlphaAccess, feedbackRoutes); +app.use("/api/upload", requireAlphaAccess, uploadRoutes); // Error handling middleware (must be last) app.use(errorLogger); @@ -195,13 +198,30 @@ sequelize }); // Fail fast - don't start server if email templates can't load if (env === "prod" || env === "production") { - logger.error("Cannot start server without email services in production"); + logger.error( + "Cannot start server without email services in production" + ); process.exit(1); } else { - logger.warn("Email services failed to initialize - continuing in dev mode"); + logger.warn( + "Email services failed to initialize - continuing in dev mode" + ); } } + // Initialize S3 service for image uploads + try { + s3Service.initialize(); + logger.info("S3 service initialized successfully"); + } catch (err) { + logger.error("Failed to initialize S3 service", { + error: err.message, + stack: err.stack, + }); + logger.error("Cannot start server without S3 service in production"); + process.exit(1); + } + // Start the payout processor const payoutJobs = PayoutProcessor.startScheduledPayouts(); logger.info("Payout processor started"); @@ -211,7 +231,8 @@ sequelize logger.info("Rental status job started"); // Start the condition check reminder job - const conditionCheckJobs = ConditionCheckReminderJob.startScheduledReminders(); + const conditionCheckJobs = + ConditionCheckReminderJob.startScheduledReminders(); logger.info("Condition check reminder job started"); server.listen(PORT, () => { diff --git a/backend/services/s3OwnershipService.js b/backend/services/s3OwnershipService.js new file mode 100644 index 0000000..e9e00e8 --- /dev/null +++ b/backend/services/s3OwnershipService.js @@ -0,0 +1,98 @@ +const { Message, ConditionCheck, Rental } = require("../models"); +const { Op } = require("sequelize"); + +/** + * Service for verifying ownership/access to S3 files + * Used to authorize signed URL requests for private content + */ +class S3OwnershipService { + /** + * Extract file type from S3 key + * @param {string} key - S3 key like "messages/uuid.jpg" + * @returns {string|null} - File type or null if unknown + */ + static getFileTypeFromKey(key) { + if (!key) return null; + const folder = key.split("/")[0]; + const folderMap = { + profiles: "profile", + items: "item", + messages: "message", + forum: "forum", + "condition-checks": "condition-check", + }; + return folderMap[folder] || null; + } + + /** + * Verify if a user can access a file + * @param {string} key - S3 key + * @param {string} userId - User ID making the request + * @returns {Promise<{authorized: boolean, reason?: string}>} + */ + static async canAccessFile(key, userId) { + const fileType = this.getFileTypeFromKey(key); + + switch (fileType) { + case "profile": + case "item": + case "forum": + // Public folders - anyone can access + return { authorized: true }; + case "message": + return this.verifyMessageAccess(key, userId); + case "condition-check": + return this.verifyConditionCheckAccess(key, userId); + default: + return { authorized: false, reason: "Unknown file type" }; + } + } + + /** + * Verify message image access - user must be sender OR receiver + * @param {string} key - S3 key + * @param {string} userId - User ID making the request + * @returns {Promise<{authorized: boolean, reason?: string}>} + */ + static async verifyMessageAccess(key, userId) { + const message = await Message.findOne({ + where: { + imageFilename: key, + [Op.or]: [{ senderId: userId }, { receiverId: userId }], + }, + }); + return { + authorized: !!message, + reason: message ? null : "Not a participant in this message", + }; + } + + /** + * Verify condition check image access - user must be rental owner OR renter + * @param {string} key - S3 key + * @param {string} userId - User ID making the request + * @returns {Promise<{authorized: boolean, reason?: string}>} + */ + static async verifyConditionCheckAccess(key, userId) { + const check = await ConditionCheck.findOne({ + where: { + imageFilenames: { [Op.contains]: [key] }, + }, + include: [ + { + model: Rental, + as: "rental", + where: { + [Op.or]: [{ ownerId: userId }, { renterId: userId }], + }, + }, + ], + }); + return { + authorized: !!check, + reason: check ? null : "Not a participant in this rental", + }; + } +} + +module.exports = S3OwnershipService; diff --git a/backend/services/s3Service.js b/backend/services/s3Service.js new file mode 100644 index 0000000..b7ba5d9 --- /dev/null +++ b/backend/services/s3Service.js @@ -0,0 +1,238 @@ +const { + S3Client, + PutObjectCommand, + GetObjectCommand, + HeadObjectCommand, +} = require("@aws-sdk/client-s3"); +const { getSignedUrl } = require("@aws-sdk/s3-request-presigner"); +const { getAWSConfig } = require("../config/aws"); +const { v4: uuidv4 } = require("uuid"); +const path = require("path"); +const logger = require("../utils/logger"); + +// Cache-Control: 24 hours for public content (allows moderation takedowns to propagate) +// Private content (messages, condition-checks) uses presigned URLs so cache doesn't matter as much +const DEFAULT_CACHE_MAX_AGE = 86400; // 24 hours in seconds + +const UPLOAD_CONFIGS = { + profile: { + folder: "profiles", + maxSize: 5 * 1024 * 1024, + cacheMaxAge: DEFAULT_CACHE_MAX_AGE, + public: true, + }, + item: { + folder: "items", + maxSize: 10 * 1024 * 1024, + cacheMaxAge: DEFAULT_CACHE_MAX_AGE, + public: true, + }, + message: { + folder: "messages", + maxSize: 5 * 1024 * 1024, + cacheMaxAge: 3600, + public: false, + }, + forum: { + folder: "forum", + maxSize: 10 * 1024 * 1024, + cacheMaxAge: DEFAULT_CACHE_MAX_AGE, + public: true, + }, + "condition-check": { + folder: "condition-checks", + maxSize: 10 * 1024 * 1024, + cacheMaxAge: 3600, + public: false, + }, +}; + +const ALLOWED_TYPES = [ + "image/jpeg", + "image/jpg", + "image/png", + "image/gif", + "image/webp", +]; +const PRESIGN_EXPIRY = 300; // 5 minutes + +class S3Service { + constructor() { + this.client = null; + this.bucket = null; + this.region = null; + this.enabled = false; + } + + /** + * Check if S3 is enabled + * @returns {boolean} + */ + isEnabled() { + return this.enabled; + } + + initialize() { + if (process.env.S3_ENABLED !== "true") { + logger.info("S3 Service disabled (S3_ENABLED !== true)"); + this.enabled = false; + return; + } + + // S3 is enabled - validate required configuration + const bucket = process.env.S3_BUCKET; + if (!bucket) { + logger.error("S3_ENABLED=true but S3_BUCKET is not set"); + process.exit(1); + } + + try { + const config = getAWSConfig(); + this.client = new S3Client({ + ...config, + // Disable automatic checksums - browser uploads can't calculate them + requestChecksumCalculation: "WHEN_REQUIRED", + }); + this.bucket = bucket; + this.region = config.region || "us-east-1"; + this.enabled = true; + logger.info("S3 Service initialized", { + bucket: this.bucket, + region: this.region, + }); + } catch (error) { + logger.error("Failed to initialize S3 Service", { error: error.message }); + process.exit(1); + } + } + + /** + * Get a presigned URL for uploading a file directly to S3 + * @param {string} uploadType - Type of upload (profile, item, message, forum, condition-check) + * @param {string} contentType - MIME type of the file + * @param {string} fileName - Original filename (used for extension) + * @param {number} fileSize - File size in bytes (required for size enforcement) + * @returns {Promise<{uploadUrl: string, key: string, publicUrl: string, expiresAt: Date}>} + */ + async getPresignedUploadUrl(uploadType, contentType, fileName, fileSize) { + if (!this.enabled) { + throw new Error("S3 storage is not enabled"); + } + + const config = UPLOAD_CONFIGS[uploadType]; + if (!config) { + throw new Error(`Invalid upload type: ${uploadType}`); + } + if (!ALLOWED_TYPES.includes(contentType)) { + throw new Error(`Invalid content type: ${contentType}`); + } + if (!fileSize || fileSize <= 0) { + throw new Error("File size is required"); + } + if (fileSize > config.maxSize) { + throw new Error( + `File too large. Maximum size is ${config.maxSize / (1024 * 1024)}MB` + ); + } + + const ext = path.extname(fileName) || this.getExtFromMime(contentType); + const key = `${config.folder}/${uuidv4()}${ext}`; + + const cacheDirective = config.public ? "public" : "private"; + const command = new PutObjectCommand({ + Bucket: this.bucket, + Key: key, + ContentType: contentType, + ContentLength: fileSize, // Enforce exact file size + CacheControl: `${cacheDirective}, max-age=${config.cacheMaxAge}`, + }); + + const uploadUrl = await getSignedUrl(this.client, command, { + expiresIn: PRESIGN_EXPIRY, + }); + + return { + uploadUrl, + key, + publicUrl: config.public + ? `https://${this.bucket}.s3.${this.region}.amazonaws.com/${key}` + : null, + expiresAt: new Date(Date.now() + PRESIGN_EXPIRY * 1000), + }; + } + + /** + * Get a presigned URL for downloading a private file from S3 + * @param {string} key - S3 object key + * @param {number} expiresIn - Expiration time in seconds (default 1 hour) + * @returns {Promise} + */ + async getPresignedDownloadUrl(key, expiresIn = 3600) { + if (!this.enabled) { + throw new Error("S3 storage is not enabled"); + } + + const command = new GetObjectCommand({ + Bucket: this.bucket, + Key: key, + }); + return getSignedUrl(this.client, command, { expiresIn }); + } + + /** + * Get the public URL for a file (only for public folders) + * @param {string} key - S3 object key + * @returns {string|null} + */ + getPublicUrl(key) { + if (!this.enabled) { + return null; + } + return `https://${this.bucket}.s3.${this.region}.amazonaws.com/${key}`; + } + + /** + * Verify that a file exists in S3 + * @param {string} key - S3 object key + * @returns {Promise} + */ + async verifyUpload(key) { + if (!this.enabled) { + return false; + } + + try { + await this.client.send( + new HeadObjectCommand({ + Bucket: this.bucket, + Key: key, + }) + ); + return true; + } catch (err) { + if (err.name === "NotFound" || err.$metadata?.httpStatusCode === 404) { + return false; + } + throw err; + } + } + + /** + * Get file extension from MIME type + * @param {string} mime - MIME type + * @returns {string} + */ + getExtFromMime(mime) { + const map = { + "image/jpeg": ".jpg", + "image/jpg": ".jpg", + "image/png": ".png", + "image/gif": ".gif", + "image/webp": ".webp", + }; + return map[mime] || ".jpg"; + } +} + +const s3Service = new S3Service(); +module.exports = s3Service; diff --git a/frontend/src/__tests__/services/api.test.ts b/frontend/src/__tests__/services/api.test.ts index 068e85b..e3abc46 100644 --- a/frontend/src/__tests__/services/api.test.ts +++ b/frontend/src/__tests__/services/api.test.ts @@ -19,8 +19,6 @@ import { feedbackAPI, fetchCSRFToken, resetCSRFToken, - getMessageImageUrl, - getForumImageUrl, } from '../../services/api'; import api from '../../services/api'; @@ -91,22 +89,6 @@ describe('API Service', () => { expect(typeof resetCSRFToken).toBe('function'); }); - it('exports helper functions for image URLs', () => { - expect(typeof getMessageImageUrl).toBe('function'); - expect(typeof getForumImageUrl).toBe('function'); - }); - }); - - describe('Helper Functions', () => { - it('getMessageImageUrl constructs correct URL', () => { - const url = getMessageImageUrl('test-image.jpg'); - expect(url).toContain('/messages/images/test-image.jpg'); - }); - - it('getForumImageUrl constructs correct URL', () => { - const url = getForumImageUrl('forum-image.jpg'); - expect(url).toContain('/uploads/forum/forum-image.jpg'); - }); }); describe('CSRF Token Management', () => { diff --git a/frontend/src/components/ChatWindow.tsx b/frontend/src/components/ChatWindow.tsx index ea2de21..60b3b49 100644 --- a/frontend/src/components/ChatWindow.tsx +++ b/frontend/src/components/ChatWindow.tsx @@ -5,7 +5,8 @@ import React, { useRef, useCallback, } from "react"; -import { messageAPI, getMessageImageUrl } from "../services/api"; +import { messageAPI } from "../services/api"; +import { getSignedUrl } from "../services/uploadService"; import { User, Message } from "../types"; import { useAuth } from "../contexts/AuthContext"; import { useSocket } from "../contexts/SocketContext"; @@ -46,6 +47,7 @@ const ChatWindow: React.FC = ({ const [hasScrolledToUnread, setHasScrolledToUnread] = useState(false); const [selectedImage, setSelectedImage] = useState(null); const [imagePreview, setImagePreview] = useState(null); + const [imageUrls, setImageUrls] = useState>(new Map()); const messagesEndRef = useRef(null); const messagesContainerRef = useRef(null); const messageRefs = useRef>(new Map()); @@ -189,6 +191,29 @@ const ChatWindow: React.FC = ({ } }, [messages, isRecipientTyping, isAtBottom, hasScrolledToUnread]); + // Pre-fetch signed URLs for private message images + useEffect(() => { + const fetchImageUrls = async () => { + const messagesWithImages = messages.filter( + (m) => m.imageFilename && !imageUrls.has(m.imageFilename) + ); + + if (messagesWithImages.length === 0) return; + + const newUrls = new Map(imageUrls); + await Promise.all( + messagesWithImages.map(async (m) => { + const url = await getSignedUrl(m.imageFilename!); + newUrls.set(m.imageFilename!, url); + }) + ); + + setImageUrls(newUrls); + }; + + fetchImageUrls(); + }, [messages]); + const fetchMessages = async () => { try { // Fetch all messages between current user and recipient @@ -525,27 +550,28 @@ const ChatWindow: React.FC = ({ wordBreak: "break-word", }} > - {message.imageFilename && ( -
- Shared image - window.open( - getMessageImageUrl(message.imageFilename!), - "_blank" - ) - } - /> -
- )} + {message.imageFilename && + imageUrls.has(message.imageFilename) && ( +
+ Shared image + window.open( + imageUrls.get(message.imageFilename!), + "_blank" + ) + } + /> +
+ )} {message.content.trim() && (

{message.content} diff --git a/frontend/src/components/CommentThread.tsx b/frontend/src/components/CommentThread.tsx index 33badea..6c53761 100644 --- a/frontend/src/components/CommentThread.tsx +++ b/frontend/src/components/CommentThread.tsx @@ -1,7 +1,7 @@ import React, { useState } from "react"; import { ForumComment } from "../types"; import CommentForm from "./CommentForm"; -import { getForumImageUrl } from "../services/api"; +import { getPublicImageUrl } from "../services/uploadService"; interface CommentThreadProps { comment: ForumComment; @@ -217,7 +217,7 @@ const CommentThread: React.FC = ({ {comment.imageFilenames.map((image, index) => (

{`Comment = ({ cursor: "pointer", }} onClick={() => - window.open(getForumImageUrl(image), "_blank") + window.open(getPublicImageUrl(image), "_blank") } />
diff --git a/frontend/src/components/ItemCard.tsx b/frontend/src/components/ItemCard.tsx index 861b25e..1e6167b 100644 --- a/frontend/src/components/ItemCard.tsx +++ b/frontend/src/components/ItemCard.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { Link } from 'react-router-dom'; import { Item } from '../types'; +import { getPublicImageUrl } from '../services/uploadService'; interface ItemCardProps { item: Item; @@ -49,12 +50,13 @@ const ItemCard: React.FC = ({
{item.imageFilenames && item.imageFilenames[0] ? ( {item.name} ) : ( diff --git a/frontend/src/components/ItemMarkerInfo.tsx b/frontend/src/components/ItemMarkerInfo.tsx index b23f3b1..c690953 100644 --- a/frontend/src/components/ItemMarkerInfo.tsx +++ b/frontend/src/components/ItemMarkerInfo.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { Item } from '../types'; +import { getPublicImageUrl } from '../services/uploadService'; interface ItemMarkerInfoProps { item: Item; @@ -31,12 +32,13 @@ const ItemMarkerInfo: React.FC = ({ item, onViewDetails })
{item.imageFilenames && item.imageFilenames[0] ? ( {item.name} diff --git a/frontend/src/pages/CreateForumPost.tsx b/frontend/src/pages/CreateForumPost.tsx index 5d81c72..684070e 100644 --- a/frontend/src/pages/CreateForumPost.tsx +++ b/frontend/src/pages/CreateForumPost.tsx @@ -2,6 +2,7 @@ import React, { useState, useEffect } from "react"; import { useNavigate, Link } from "react-router-dom"; import { useAuth } from "../contexts/AuthContext"; import { forumAPI, addressAPI } from "../services/api"; +import { uploadFiles } from "../services/uploadService"; import TagInput from "../components/TagInput"; import ForumImageUpload from "../components/ForumImageUpload"; import { Address } from "../types"; @@ -151,36 +152,53 @@ const CreateForumPost: React.FC = () => { try { setIsSubmitting(true); - // Create FormData - const submitData = new FormData(); - submitData.append('title', formData.title); - submitData.append('content', formData.content); - submitData.append('category', formData.category); + // Upload images to S3 first (if any) + let imageFilenames: string[] = []; + if (imageFiles.length > 0) { + const uploadResults = await uploadFiles("forum", imageFiles); + imageFilenames = uploadResults.map((result) => result.key); + } - // Add tags as JSON string + // Build the post data + const postData: { + title: string; + content: string; + category: string; + tags?: string[]; + zipCode?: string; + latitude?: number; + longitude?: number; + imageFilenames?: string[]; + } = { + title: formData.title, + content: formData.content, + category: formData.category, + }; + + // Add tags if present if (formData.tags.length > 0) { - submitData.append('tags', JSON.stringify(formData.tags)); + postData.tags = formData.tags; } // Add location data for item requests if (formData.category === 'item_request' && formData.zipCode) { - submitData.append('zipCode', formData.zipCode); + postData.zipCode = formData.zipCode; // If we have coordinates from a saved address, send them to avoid re-geocoding if (formData.latitude !== undefined && formData.longitude !== undefined) { - submitData.append('latitude', formData.latitude.toString()); - submitData.append('longitude', formData.longitude.toString()); + postData.latitude = formData.latitude; + postData.longitude = formData.longitude; } } - // Add images - imageFiles.forEach((file) => { - submitData.append('images', file); - }); + // Add S3 image keys + if (imageFilenames.length > 0) { + postData.imageFilenames = imageFilenames; + } - const response = await forumAPI.createPost(submitData); + const response = await forumAPI.createPost(postData); navigate(`/forum/${response.data.id}`); } catch (err: any) { - setError(err.response?.data?.error || "Failed to create post"); + setError(err.response?.data?.error || err.message || "Failed to create post"); setIsSubmitting(false); } }; diff --git a/frontend/src/pages/CreateItem.tsx b/frontend/src/pages/CreateItem.tsx index 1703b37..adf78c4 100644 --- a/frontend/src/pages/CreateItem.tsx +++ b/frontend/src/pages/CreateItem.tsx @@ -2,6 +2,7 @@ import React, { useState, useEffect, useRef } from "react"; import { useNavigate } from "react-router-dom"; import { useAuth } from "../contexts/AuthContext"; import api, { addressAPI, userAPI, itemAPI } from "../services/api"; +import { uploadFiles } from "../services/uploadService"; import AvailabilitySettings from "../components/AvailabilitySettings"; import ImageUpload from "../components/ImageUpload"; import ItemInformation from "../components/ItemInformation"; @@ -175,9 +176,12 @@ const CreateItem: React.FC = () => { } try { - // For now, we'll store image URLs as base64 strings - // In production, you'd upload to a service like S3 - const imageUrls = imagePreviews; + // Upload images to S3 first + let imageFilenames: string[] = []; + if (imageFiles.length > 0) { + const uploadResults = await uploadFiles("item", imageFiles); + imageFilenames = uploadResults.map((result) => result.key); + } // Construct location from address components const locationParts = [ @@ -216,7 +220,7 @@ const CreateItem: React.FC = () => { specifyTimesPerDay: formData.specifyTimesPerDay, weeklyTimes: formData.weeklyTimes, location, - images: imageUrls, + imageFilenames, }); // Auto-save address if user has no addresses and entered manual address @@ -260,7 +264,7 @@ const CreateItem: React.FC = () => { navigate(`/items/${response.data.id}`); } catch (err: any) { - setError(err.response?.data?.error || "Failed to create listing"); + setError(err.response?.data?.error || err.message || "Failed to create listing"); } finally { setLoading(false); } diff --git a/frontend/src/pages/EditItem.tsx b/frontend/src/pages/EditItem.tsx index f657a9c..0044e50 100644 --- a/frontend/src/pages/EditItem.tsx +++ b/frontend/src/pages/EditItem.tsx @@ -3,6 +3,7 @@ import { useParams, useNavigate } from "react-router-dom"; import { Item, Rental, Address } from "../types"; import { useAuth } from "../contexts/AuthContext"; import { itemAPI, rentalAPI, addressAPI, userAPI } from "../services/api"; +import { uploadFiles, getPublicImageUrl } from "../services/uploadService"; import AvailabilitySettings from "../components/AvailabilitySettings"; import ImageUpload from "../components/ImageUpload"; import ItemInformation from "../components/ItemInformation"; @@ -53,6 +54,7 @@ const EditItem: React.FC = () => { const [success, setSuccess] = useState(false); const [imageFiles, setImageFiles] = useState([]); const [imagePreviews, setImagePreviews] = useState([]); + const [existingImageKeys, setExistingImageKeys] = useState([]); // S3 keys for existing images const [acceptedRentals, setAcceptedRentals] = useState([]); const [userAddresses, setUserAddresses] = useState([]); const [selectedAddressId, setSelectedAddressId] = useState(""); @@ -161,9 +163,11 @@ const EditItem: React.FC = () => { }, }); - // Set existing images as previews + // Set existing images - store S3 keys and generate preview URLs if (item.imageFilenames && item.imageFilenames.length > 0) { - setImagePreviews(item.imageFilenames); + setExistingImageKeys(item.imageFilenames); + // Generate preview URLs from S3 keys + setImagePreviews(item.imageFilenames.map((key: string) => getPublicImageUrl(key))); } // Determine which pricing unit to select based on existing data @@ -270,8 +274,15 @@ const EditItem: React.FC = () => { } try { - // Use existing image previews (which includes both old and new images) - const imageUrls = imagePreviews; + // Upload new images to S3 and get their keys + let newImageKeys: string[] = []; + if (imageFiles.length > 0) { + const uploadResults = await uploadFiles("item", imageFiles); + newImageKeys = uploadResults.map((result) => result.key); + } + + // Combine existing S3 keys with newly uploaded keys + const allImageKeys = [...existingImageKeys, ...newImageKeys]; const updatePayload = { ...formData, @@ -297,7 +308,7 @@ const EditItem: React.FC = () => { availableBefore: formData.generalAvailableBefore, specifyTimesPerDay: formData.specifyTimesPerDay, weeklyTimes: formData.weeklyTimes, - images: imageUrls, + imageFilenames: allImageKeys, }; await itemAPI.updateItem(id!, updatePayload); @@ -328,7 +339,7 @@ const EditItem: React.FC = () => { navigate(`/items/${id}`); }, 1500); } catch (err: any) { - setError(err.response?.data?.message || "Failed to update item"); + setError(err.response?.data?.message || err.message || "Failed to update item"); } }; @@ -355,6 +366,16 @@ const EditItem: React.FC = () => { }; const removeImage = (index: number) => { + // Check if removing an existing image or a new upload + if (index < existingImageKeys.length) { + // Removing an existing S3 image + setExistingImageKeys((prev) => prev.filter((_, i) => i !== index)); + } else { + // Removing a new upload - adjust index for the imageFiles array + const newFileIndex = index - existingImageKeys.length; + setImageFiles((prev) => prev.filter((_, i) => i !== newFileIndex)); + } + // Always update previews setImagePreviews((prev) => prev.filter((_, i) => i !== index)); }; diff --git a/frontend/src/pages/ForumPostDetail.tsx b/frontend/src/pages/ForumPostDetail.tsx index a745a1f..b4f902a 100644 --- a/frontend/src/pages/ForumPostDetail.tsx +++ b/frontend/src/pages/ForumPostDetail.tsx @@ -1,7 +1,8 @@ import React, { useState, useEffect } from 'react'; import { useParams, useNavigate, Link, useSearchParams } from 'react-router-dom'; import { useAuth } from '../contexts/AuthContext'; -import { forumAPI, getForumImageUrl } from '../services/api'; +import { forumAPI } from '../services/api'; +import { uploadFiles, getPublicImageUrl } from '../services/uploadService'; import { ForumPost, ForumComment } from '../types'; import CategoryBadge from '../components/CategoryBadge'; import PostStatusBadge from '../components/PostStatusBadge'; @@ -54,17 +55,20 @@ const ForumPostDetail: React.FC = () => { } try { - const formData = new FormData(); - formData.append('content', content); + // Upload images to S3 first (if any) + let imageFilenames: string[] = []; + if (images.length > 0) { + const uploadResults = await uploadFiles("forum", images); + imageFilenames = uploadResults.map((result) => result.key); + } - images.forEach((file) => { - formData.append('images', file); + await forumAPI.createComment(id!, { + content, + imageFilenames: imageFilenames.length > 0 ? imageFilenames : undefined, }); - - await forumAPI.createComment(id!, formData); await fetchPost(); // Refresh to get new comment } catch (err: any) { - throw new Error(err.response?.data?.error || 'Failed to post comment'); + throw new Error(err.response?.data?.error || err.message || 'Failed to post comment'); } }; @@ -75,14 +79,13 @@ const ForumPostDetail: React.FC = () => { } try { - const formData = new FormData(); - formData.append('content', content); - formData.append('parentCommentId', parentCommentId); - - await forumAPI.createComment(id!, formData); + await forumAPI.createComment(id!, { + content, + parentId: parentCommentId, + }); await fetchPost(); // Refresh to get new reply } catch (err: any) { - throw new Error(err.response?.data?.error || 'Failed to post reply'); + throw new Error(err.response?.data?.error || err.message || 'Failed to post reply'); } }; @@ -348,11 +351,11 @@ const ForumPostDetail: React.FC = () => { {post.imageFilenames.map((image, index) => (
{`Post window.open(getForumImageUrl(image), '_blank')} + onClick={() => window.open(getPublicImageUrl(image), '_blank')} />
))} diff --git a/frontend/src/pages/ItemDetail.tsx b/frontend/src/pages/ItemDetail.tsx index 09d3dce..fb8b8f6 100644 --- a/frontend/src/pages/ItemDetail.tsx +++ b/frontend/src/pages/ItemDetail.tsx @@ -3,6 +3,7 @@ import { useParams, useNavigate } from "react-router-dom"; import { Item, Rental } from "../types"; import { useAuth } from "../contexts/AuthContext"; import { itemAPI, rentalAPI } from "../services/api"; +import { getPublicImageUrl } from "../services/uploadService"; import GoogleMapWithRadius from "../components/GoogleMapWithRadius"; import ItemReviews from "../components/ItemReviews"; import ConfirmationModal from "../components/ConfirmationModal"; @@ -417,13 +418,14 @@ const ItemDetail: React.FC = () => { {item.imageFilenames.length > 0 ? (
{item.name} {item.imageFilenames.length > 1 && ( @@ -431,7 +433,7 @@ const ItemDetail: React.FC = () => { {item.imageFilenames.map((image, index) => ( {`${item.name} {
{rental.item?.imageFilenames && rental.item.imageFilenames[0] && ( {rental.item.name} )}
@@ -529,10 +530,10 @@ const Owning: React.FC = () => { > {item.imageFilenames && item.imageFilenames[0] && ( {item.name} )}
diff --git a/frontend/src/pages/Profile.tsx b/frontend/src/pages/Profile.tsx index 9b30bc6..858963b 100644 --- a/frontend/src/pages/Profile.tsx +++ b/frontend/src/pages/Profile.tsx @@ -3,7 +3,7 @@ import { useNavigate } from "react-router-dom"; import { useAuth } from "../contexts/AuthContext"; import { userAPI, itemAPI, rentalAPI, addressAPI } from "../services/api"; import { User, Item, Rental, Address } from "../types"; -import { getImageUrl } from "../utils/imageUrl"; +import { uploadFile, getPublicImageUrl } from "../services/uploadService"; import AvailabilitySettings from "../components/AvailabilitySettings"; import ReviewItemModal from "../components/ReviewModal"; import ReviewRenterModal from "../components/ReviewRenterModal"; @@ -161,7 +161,7 @@ const Profile: React.FC = () => { response.data.itemRequestNotificationRadius || 10, }); if (response.data.imageFilename) { - setImagePreview(getImageUrl(response.data.imageFilename)); + setImagePreview(getPublicImageUrl(response.data.imageFilename)); } } catch (err: any) { setError(err.response?.data?.message || "Failed to fetch profile"); @@ -301,29 +301,26 @@ const Profile: React.FC = () => { }; reader.readAsDataURL(file); - // Upload image immediately + // Upload image to S3 try { - const formData = new FormData(); - formData.append("imageFilename", file); + const { key, publicUrl } = await uploadFile("profile", file); - const response = await userAPI.uploadProfileImage(formData); - - // Update the imageFilename in formData with the new filename + // Update the imageFilename in formData with the S3 key setFormData((prev) => ({ ...prev, - imageFilename: response.data.filename, + imageFilename: key, })); - // Update preview to use the uploaded image URL - setImagePreview(getImageUrl(response.data.imageUrl)); + // Update preview to use the S3 URL + setImagePreview(publicUrl); } catch (err: any) { console.error("Image upload error:", err); - setError(err.response?.data?.error || "Failed to upload image"); + setError(err.message || "Failed to upload image"); // Reset on error setImageFile(null); setImagePreview( profileData?.imageFilename - ? getImageUrl(profileData.imageFilename) + ? getPublicImageUrl(profileData.imageFilename) : null ); } @@ -384,7 +381,7 @@ const Profile: React.FC = () => { profileData.itemRequestNotificationRadius || 10, }); setImagePreview( - profileData.imageFilename ? getImageUrl(profileData.imageFilename) : null + profileData.imageFilename ? getPublicImageUrl(profileData.imageFilename) : null ); } }; @@ -1224,7 +1221,7 @@ const Profile: React.FC = () => {
{rental.item?.imageFilenames && rental.item.imageFilenames[0] && ( {rental.item.name} {
{rental.item?.imageFilenames && rental.item.imageFilenames[0] && ( {rental.item.name} { > {item.imageFilenames.length > 0 ? ( {item.name} ) : (
diff --git a/frontend/src/pages/RentItem.tsx b/frontend/src/pages/RentItem.tsx index d9a031c..d5b834a 100644 --- a/frontend/src/pages/RentItem.tsx +++ b/frontend/src/pages/RentItem.tsx @@ -3,6 +3,7 @@ import { useParams, useNavigate, useSearchParams } from "react-router-dom"; import { Item } from "../types"; import { useAuth } from "../contexts/AuthContext"; import { itemAPI, rentalAPI } from "../services/api"; +import { getPublicImageUrl } from "../services/uploadService"; import EmbeddedStripeCheckout from "../components/EmbeddedStripeCheckout"; const RentItem: React.FC = () => { @@ -343,13 +344,14 @@ const RentItem: React.FC = () => {
{item.imageFilenames && item.imageFilenames[0] && ( {item.name} )} diff --git a/frontend/src/pages/Renting.tsx b/frontend/src/pages/Renting.tsx index ec3b513..224c263 100644 --- a/frontend/src/pages/Renting.tsx +++ b/frontend/src/pages/Renting.tsx @@ -2,6 +2,7 @@ import React, { useState, useEffect } from "react"; import { Link, useNavigate } from "react-router-dom"; import { useAuth } from "../contexts/AuthContext"; import { rentalAPI, conditionCheckAPI } from "../services/api"; +import { getPublicImageUrl } from "../services/uploadService"; import { Rental } from "../types"; import ReviewItemModal from "../components/ReviewModal"; import RentalCancellationModal from "../components/RentalCancellationModal"; @@ -232,10 +233,10 @@ const Renting: React.FC = () => { > {rental.item?.imageFilenames && rental.item.imageFilenames[0] && ( {rental.item.name} )}
diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 317b175..ebcc69f 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -261,12 +261,16 @@ export const messageAPI = { export const forumAPI = { getPosts: (params?: any) => api.get("/forum/posts", { params }), getPost: (id: string) => api.get(`/forum/posts/${id}`), - createPost: (formData: FormData) => - api.post("/forum/posts", formData, { - headers: { - "Content-Type": "multipart/form-data", - }, - }), + createPost: (data: { + title: string; + content: string; + category: string; + tags?: string[]; + zipCode?: string; + latitude?: number; + longitude?: number; + imageFilenames?: string[]; + }) => api.post("/forum/posts", data), updatePost: (id: string, data: any) => api.put(`/forum/posts/${id}`, data), deletePost: (id: string) => api.delete(`/forum/posts/${id}`), updatePostStatus: (id: string, status: string) => @@ -275,12 +279,14 @@ export const forumAPI = { api.patch(`/forum/posts/${postId}/accept-answer`, { commentId }), getMyPosts: () => api.get("/forum/my-posts"), getTags: (params?: any) => api.get("/forum/tags", { params }), - createComment: (postId: string, formData: FormData) => - api.post(`/forum/posts/${postId}/comments`, formData, { - headers: { - "Content-Type": "multipart/form-data", - }, - }), + createComment: ( + postId: string, + data: { + content: string; + parentId?: string; + imageFilenames?: string[]; + } + ) => api.post(`/forum/posts/${postId}/comments`, data), updateComment: (commentId: string, data: any) => api.put(`/forum/comments/${commentId}`, data), deleteComment: (commentId: string) => @@ -342,12 +348,4 @@ export const feedbackAPI = { api.post("/feedback", data), }; -// Helper to construct message image URLs -export const getMessageImageUrl = (imagePath: string) => - `${API_BASE_URL}/messages/images/${imagePath}`; - -// Helper to construct forum image URLs -export const getForumImageUrl = (imagePath: string) => - `${process.env.REACT_APP_BASE_URL}/uploads/forum/${imagePath}`; - export default api; diff --git a/frontend/src/services/uploadService.ts b/frontend/src/services/uploadService.ts new file mode 100644 index 0000000..89bd8e1 --- /dev/null +++ b/frontend/src/services/uploadService.ts @@ -0,0 +1,195 @@ +import api from "./api"; + +/** + * Get the public URL for an image (S3 only) + */ +export const getPublicImageUrl = ( + imagePath: string | null | undefined +): string => { + if (!imagePath) return ""; + + // Already a full S3 URL + if (imagePath.startsWith("https://") && imagePath.includes("s3.")) { + return imagePath; + } + + // S3 key (e.g., "profiles/uuid.jpg", "items/uuid.jpg", "forum/uuid.jpg") + const s3Bucket = process.env.REACT_APP_S3_BUCKET || ""; + const s3Region = process.env.REACT_APP_AWS_REGION || "us-east-1"; + return `https://${s3Bucket}.s3.${s3Region}.amazonaws.com/${imagePath}`; +}; + +export interface PresignedUrlResponse { + uploadUrl: string; + key: string; + publicUrl: string; + expiresAt: string; +} + +export type UploadType = + | "profile" + | "item" + | "message" + | "forum" + | "condition-check"; + +interface UploadOptions { + onProgress?: (percent: number) => void; + maxRetries?: number; +} + +/** + * Get a presigned URL for uploading a single file + */ +export async function getPresignedUrl( + uploadType: UploadType, + file: File +): Promise { + const response = await api.post("/upload/presign", { + uploadType, + contentType: file.type, + fileName: file.name, + fileSize: file.size, + }); + return response.data; +} + +/** + * Get presigned URLs for uploading multiple files + */ +export async function getPresignedUrls( + uploadType: UploadType, + files: File[] +): Promise { + const response = await api.post("/upload/presign-batch", { + uploadType, + files: files.map((f) => ({ + contentType: f.type, + fileName: f.name, + fileSize: f.size, + })), + }); + return response.data.uploads; +} + +/** + * Upload a file directly to S3 using a presigned URL + */ +export async function uploadToS3( + file: File, + uploadUrl: string, + options: UploadOptions = {} +): Promise { + const { onProgress, maxRetries = 3 } = options; + + for (let attempt = 0; attempt < maxRetries; attempt++) { + try { + await new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.open("PUT", uploadUrl, true); + xhr.setRequestHeader("Content-Type", file.type); + + if (onProgress) { + xhr.upload.onprogress = (e) => { + if (e.lengthComputable) { + onProgress(Math.round((e.loaded / e.total) * 100)); + } + }; + } + + xhr.onload = () => { + if (xhr.status >= 200 && xhr.status < 300) { + resolve(); + } else { + reject(new Error(`HTTP ${xhr.status}`)); + } + }; + + xhr.onerror = () => reject(new Error("Network error")); + xhr.send(file); + }); + return; + } catch (error) { + if (attempt === maxRetries - 1) throw error; + // Exponential backoff + await new Promise((r) => setTimeout(r, Math.pow(2, attempt) * 1000)); + } + } +} + +/** + * Confirm that files have been uploaded to S3 + */ +export async function confirmUploads( + keys: string[] +): Promise<{ confirmed: string[]; total: number }> { + const response = await api.post("/upload/confirm", { keys }); + return response.data; +} + +/** + * Upload a single file to S3 (complete flow) + */ +export async function uploadFile( + uploadType: UploadType, + file: File, + options: UploadOptions = {} +): Promise<{ key: string; publicUrl: string }> { + // Get presigned URL + const presigned = await getPresignedUrl(uploadType, file); + + // Upload to S3 + await uploadToS3(file, presigned.uploadUrl, options); + + // Confirm upload + const { confirmed } = await confirmUploads([presigned.key]); + + if (confirmed.length === 0) { + throw new Error("Upload verification failed"); + } + + return { key: presigned.key, publicUrl: presigned.publicUrl }; +} + +/** + * Upload multiple files to S3 (complete flow) + */ +export async function uploadFiles( + uploadType: UploadType, + files: File[], + options: UploadOptions = {} +): Promise<{ key: string; publicUrl: string }[]> { + if (files.length === 0) return []; + + // Get presigned URLs for all files + const presignedUrls = await getPresignedUrls(uploadType, files); + + // Upload all files in parallel + await Promise.all( + files.map((file, i) => + uploadToS3(file, presignedUrls[i].uploadUrl, options) + ) + ); + + // Confirm all uploads + const keys = presignedUrls.map((p) => p.key); + const { confirmed, total } = await confirmUploads(keys); + + if (confirmed.length < total) { + console.warn(`${total - confirmed.length} uploads failed verification`); + } + + return presignedUrls + .filter((p) => confirmed.includes(p.key)) + .map((p) => ({ key: p.key, publicUrl: p.publicUrl })); +} + +/** + * Get a signed URL for accessing private content (messages, condition-checks) + */ +export async function getSignedUrl(key: string): Promise { + const response = await api.get( + `/upload/signed-url/${encodeURIComponent(key)}` + ); + return response.data.url; +} diff --git a/frontend/src/utils/imageUrl.ts b/frontend/src/utils/imageUrl.ts deleted file mode 100644 index 366e974..0000000 --- a/frontend/src/utils/imageUrl.ts +++ /dev/null @@ -1,13 +0,0 @@ -export const getImageUrl = (imagePath: string): string => { - // Get the base URL without /api - const apiUrl = process.env.REACT_APP_API_URL || ''; - const baseUrl = apiUrl.replace('/api', ''); - - // If imagePath already includes the full path, use it - if (imagePath.startsWith('/uploads/')) { - return `${baseUrl}${imagePath}`; - } - - // Otherwise, construct the full path - return `${baseUrl}/uploads/profiles/${imagePath}`; -}; \ No newline at end of file