diff --git a/.env.example b/.env.example deleted file mode 100644 index 848ad61..0000000 --- a/.env.example +++ /dev/null @@ -1,3 +0,0 @@ -DATABASE_URL=postgres://hostname:5432/database-name -JWT_SECRET=super-secret-jwt-key -FRONTEND_URL=http://localhost:3000 diff --git a/api/Amazon_S3_Integration.md b/api/Amazon_S3_Integration.md new file mode 100644 index 0000000..c89c37e --- /dev/null +++ b/api/Amazon_S3_Integration.md @@ -0,0 +1,193 @@ +# Reason + +I have very little hope I will actually remember the steps I took to complete the Amazon s3 task. + +Below is a step by step breakdown of the process, inspired by [this video](https://www.youtube.com/watch?v=eQAIojcArRY&t=996s) + +## Quick Setup + +If you just want to setup on your local machine S3 access, go to [Bucket Access](#bucket-access) and copy and paste the `.env` variables into your `.env` file. The `ACCESS_KEY` and `SECRET_ACCESS_KEY` and `BUCKET_NAME` are not in this file for _obvious_ reasons(hiding bucket name might just me being paranoid). If the `SECRET_ACCESS_KEY` ends up getting _lost_, then I will have to make a new **IAM** user, so please let me know. + +Next install the npm packages if you haven't done this setup before. This way you install the packages in [Express Server File Handling](#express-server-file-handling) and [Amazon S3 SDK](#amazon-s3-sdk). + +```bash +npm install +``` + +Thats it! You can now run requests to the Amazon S3 bucket, and our PostgreSQL DB. If you run into any issues, please reach out to me @EmmanuelR21. + +## Bucket Access + +1. Our bucket is essentially the storage DB that holds the images and video files. The media files are known as **"Objects"** +2. There is an Amazon **IAM** user known as `echo-cache`, which has 3 permissions(given by a custom policy I created), they are: + 1. `PUT` object to DB. (They do not call it POST for some reason) + 2. `GET` object from DB. + 3. `DELETE` object from DB. + +**_IMPORTANT: To be able to use the user within our express server, the following PRIVATE keys must be used in an `.env` file:_** + +```env +BUCKET_NAME='capstone-2-echo-cache' +BUCKET_REGION='us-east-2' +ACCESS_KEY='' +SECRET_ACCESS_KEY='' +``` + +## Express Server File Handling + +Our express server will not understand how to deal with `multipart/form-data`(This is the `Content-Type` of the media we send through the ``) by default, so we will need to use a middleware known as `multer`. We will also need `sharp` which is a package we will use for image/video resizing. `sharp` isn't strictly necessary but it would be nice to have all of our video and image formats in a portrait mode. + +```bash +npm install multer sharp # sharp is not strictly required +``` + +Next put in some of these important variables. **NOTE:** that the `crypto` variable is imported from a built in Javascript library, so there should be no need to install `crypto`. It will be used later for unique ID creation. + +```javascript +const multer = require("multer"); +const crypto = require("crypto"); +const sharp = require("sharp"); // To create a resized image, for example to put images into portrait mode. + +const storage = multer.memoryStorage(); //This designates the server to store the media in memory, instead of on disk. +const upload = multer({ storage: storage }); +``` + +Our `upload` variable will be used later in our route as a middleware. + +## Amazon S3 SDK + +After having created an access point to the bucket, now what is needed is to install to the express server `@aws-sdk/client-s3` for creating `POST` request to our Bucket, and `@aws-sdk/s3-request-presigner` for `GET` requests to the Bucket: + +```bash +npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner +``` + +After installing, paste these variables into the header of the file, these will all serve a purpose for our `GET`, `POST` and `DELETE`: + +```javascript +const { + S3Client, + PutObjectCommand, + GetObjectCommand, + DeleteObjectsCommand, +} = require("@aws-sdk/client-s3"); +const { getSignedUrl } = require("@aws-sdk/s3-request-presigner"); +``` + +Next, we will need to create an `s3Client` using our `.env` variables: + +```javascript +const bucketName = process.env.BUCKET_NAME; +const bucketRegion = process.env.BUCKET_REGION; +const accessKey = process.env.ACCESS_KEY; +const secretAccessKey = process.env.SECRET_ACCESS_KEY; + +const s3 = new S3Client({ + credentials: { + accessKeyId: accessKey, + secretAccessKey: secretAccessKey, + }, + region: bucketRegion, +}); +``` + +### Quick **IMPORTANT** note + +Its important I clarify a few things: + +1. Our Amazon s3 bucket exists on my AWS account, with a free tier for 3 months more or less, or until my credits run out, which I have ample enough for our testing phase. Please see number 3 :) +2. The Sequelize related models functions/method names are all guesswork for now, as I have not fully implemented the activity within our own DB. I will update with proper naming, and behavior when it gets implemented officially. +3. There is **NO** validation being done right now. In theory someone could upload a zetabyte of horse videos and my AWS account bill will go 📈. _Please_ do **NOT** do this and be cautious on the size of content you are uploading. If we reach double digits in GB's, please let me know you are doing this so I can monitor my usage on AWS. Thanks :)! +4. I did not keep security in mind when developing the routes as I was moving quickly to develop the rough idea. I will update this as we continue developing the routes. + +### Posting to our Bucket + +```javascript +/* +- upload.array() will take an array of "files" named "media". The name "media" can be literally anything, just depends on what you name the array of files being sent from the client. +- upload.array() will store the file buffers in req.files when it is done. +*/ +router.post( + "/", + [authenticateJWT, upload.array("media", 10)], + async (req, res) => { + // We use UUID's to prevent file name collision, which will overwrite one file over the other. We will also use it to retrieve the media later so we store it in our Postgres DB. + const image_uuids = []; + // S3 does not allow you to upload several files at once, so we have to loop + for (let i = 0; i < req.files.length; i++) { + const buffer = await sharp(req.files[i].buffer) + .resize({ height: 1920, width: 1080, fit: "contain" }) + .toBuffer(); //This is for resizing an image to portrait mode. The image wont get affected, but black bars will fill the gaps. + const uniqueImgId = crypto.randomUUID(); + const params = { + Bucket: bucketName, + Key: uniqueImgId, + Body: buffer, //This is strictly for image resizing, if uploading videos you would have to use req.files[i].buffer and or use another package to resize the video. + ContentType: req.files[i].mimetype, + }; + + const command = new PutObjectCommand(params); + // The following uses the s3Client variable we made earlier, to store to the AWS DB bucket + await s3.send(command); + // Upon successful completion we also save the UUID in our array + image_uuids.push(uniqueImgId); + } + await Echoes.create({ + /* + Include here all relevant info for creating the echo + */ + image_uuids, + }); + + res.send(post); + }, +); +``` + +### Getting from our Bucket + +```javascript +router.get("/:id", authenticateJWT, async (req, res) => { + const echo = await Echo.getByPk(echoId); + const signed_urls = []; + for (let i = 0; i < echo.image_uuids.length; i++) { + const objectParams = { + Bucket: bucketName, + Key: echo.image_uuids[i], + }; + const command = new GetObjectCommand(objectParams); + const url = await getSignedUrl(s3, command, { expiresIn: 3600 }); + signed_urls.push(url); + } + // I prefer this method as opposed to using Sequelize's "update" method, as the AWS signed url is rather long, and might throw an error to the server (although it will still attach itself properly) + echo.signed_urls = signed_urls; + res.send(echo); +}); +``` + +### Deleting from our Bucket + +```javascript +router.delete("/:id", async (req, res) => { + const echoId = req.params.id; + const echo = await Echo.findByPk(echoId); + const params = { + Bucket: bucketName, + // Takes an array called "Objects" which will have the array of the multiple uuids, to delete all at once. + Delete: { + Objects: echo.uuids.map((uuid) => ({ Key: uuid })), + }, + }; + // Delete multiple objects from the bucket + const command = new DeleteObjectsCommand(params); + await s3.send(command); + + // Sequelize deletes from our db here + await echo.destroy(); + res.send({}); +}); +``` + +## Final Note + +I'm so happy I'm done with this 😭 diff --git a/api/echoes.js b/api/echoes.js new file mode 100644 index 0000000..a215f9a --- /dev/null +++ b/api/echoes.js @@ -0,0 +1,526 @@ +require("dotenv").config(); +const express = require("express"); +const router = express.Router(); +const { Echoes, Echo_recipients, Friends, Media } = require("../database"); +const { authenticateJWT } = require("../auth"); +const { Op } = require("sequelize"); +const { response } = require("../app"); +const multer = require("multer"); +const crypto = require("crypto"); +const { + S3Client, + GetObjectCommand, + DeleteObjectsCommand, +} = require("@aws-sdk/client-s3"); +const { getSignedUrl } = require("@aws-sdk/s3-request-presigner"); +const { Upload } = require("@aws-sdk/lib-storage"); +const storage = multer.memoryStorage(); +const upload = multer({ storage: storage }); +const bucketName = process.env.BUCKET_NAME; +const bucketRegion = process.env.BUCKET_REGION; +const accessKey = process.env.ACCESS_KEY; +const secretAccessKey = process.env.SECRET_ACCESS_KEY; + +const s3 = new S3Client({ + credentials: { + accessKeyId: accessKey, + secretAccessKey: secretAccessKey, + }, + region: bucketRegion, +}); + +/* ---------- helpers ---------- */ +// helper: check if two users are friends (accepted) +async function isFriendWith(db, meId, otherId) { + if (meId === otherId) return true; + const rel = await Friends.findOne({ + where: { + [Op.or]: [ + { user_id: otherId, friend_id: meId }, + { user_id: meId, friend_id: otherId }, + ], + status: "accepted", + }, + }); + return !!rel; +} + +// helper: check if user is a recipient in Echo_recipients +async function isRecipientOf(db, echoId, meId) { + const rec = await Echo_recipients.findOne({ + where: { echo_id: echoId, recipient_id: meId }, + }); + return !!rec; +} + +/* ========================================================= + HOME FEED + (must be BEFORE /:id to avoid treating 'home' as ID) + Visible to signed-in user: + - public + - friend (if accepted friendship OR you’re the author) + - custom (if you’re a recipient OR you’re the author) + - self (only if you’re the author) + ========================================================= */ +router.get("/home", authenticateJWT, async (req, res) => { + try { + const user_id = req.user.id; + + const all = await Echoes.findAll(); + const visible = []; + + for (const e of all) { + const own = e.user_id === user_id; + + if (e.recipient_type === "public") { + visible.push(e); + continue; + } + if (e.recipient_type === "self") { + if (own) visible.push(e); + continue; + } + if (e.recipient_type === "friend") { + if (own || (await isFriendWith(null, user_id, e.user_id))) visible.push(e); + continue; + } + if (e.recipient_type === "custom") { + if (own || (await isRecipientOf(null, e.id, user_id))) visible.push(e); + continue; + } + } + + res.json(visible); + } catch (err) { + console.error("GET /api/echoes/home error:", err); + res.status(500).json({ error: "Failed to load homepage echoes" }); + } +}); + +/* ========================================================= + DASHBOARD LISTS (Inbox / Saved) + - Inbox: own OR friends OR custom-where-recipient + - Saved: (stub) return [] until you add a Saved table/flag + ========================================================= */ +router.get("/", authenticateJWT, async (req, res) => { + try { + const user_id = req.user.id; + const tab = (req.query.tab || "Inbox").toLowerCase(); + + if (tab === "saved") { + // TODO: wire to real saved/bookmark table/flag + return res.json([]); + } + + // Inbox + const all = await Echoes.findAll(); + const inbox = []; + + for (const e of all) { + const own = e.user_id === user_id; + + if (own) { inbox.push(e); continue; } + + if (e.recipient_type === "friend") { + if (await isFriendWith(null, user_id, e.user_id)) { inbox.push(e); continue; } + } + + if (e.recipient_type === "custom") { + if (await isRecipientOf(null, e.id, user_id)) { inbox.push(e); continue; } + } + + // exclude public/self by others from Inbox per your spec + } + + res.json(inbox); + } catch (err) { + console.error("GET /api/echoes error:", err); + res.status(500).json({ error: "Failed to fetch echoes" }); + } +}); + +/* ========================================================= + fetching echo by id + ========================================================= */ +router.get("/:id", authenticateJWT, async (req, res) => { + try { + const user_id = req.user.id; + const echo = await Echoes.findByPk(req.params.id, { + include: { model: Media }, + }); + + if (!echo) { + return res.status(404).json({ error: "Echo not found" }); + } + + const isCreator = echo.user_id === user_id; + + for (let i = 0; i < echo.media.length; i++) { + const objectParams = { + Bucket: bucketName, + Key: echo.media[i].uuid, + }; + const command = new GetObjectCommand(objectParams); + const url = await getSignedUrl(s3, command, { expiresIn: 3600 }); + await echo.media[i].update({ signed_url: url }); + } + + // If echo is only visible to self and the user is the creator + if (echo.recipient_type === "self") { + if (isCreator) { + return res.json(echo); + } else { + return res.status(403).json({ private: "This echo is private" }); + } + } + + // If echo is public, only show if unlocked + if (echo.recipient_type === "public") { + if (echo.is_unlocked) { + return res.json(echo); + } else if (isCreator) { + return res.json(echo); + } else { + return res.status(403).json({ locked: "Echo is locked" }); + } + } + + // if echo is for friends and is unlocked + if (echo.recipient_type === "friend") { + if (isCreator) { + return res.json(echo); // creator can always view + } + + // check if friendship is accepted between echo creator and logged in user + const isFriend = await Friends.findOne({ + where: { + [Op.or]: [ + { user_id: echo.user_id, friend_id: user_id }, + { user_id: user_id, friend_id: echo.user_id }, + ], + status: "accepted", + }, + }); + + if (isFriend) { + if (echo.is_unlocked) { + return res.json(echo); + } else { + return res.status(403).json({ locked: "Echo is locked" }); + } + } else { + return res.status(403).json({ + not_friend: "This echo is only accessible to friends of echo creator", + }); + } + } + + // if echo is custom and for specific users + if (echo.recipient_type === "custom") { + if (isCreator) { + return res.json(echo); + } + const isRecipient = await Echo_recipients.findOne({ + where: { + echo_id: echo.id, + recipient_id: user_id, + }, + }); + + if (isRecipient && echo.is_unlocked) { + return res.json(echo); + } else if (isRecipient && !echo.is_unlocked) { + return res.status(403).json({ locked: "Echo is locked" }); + } + } + + // default: no access + res.status(403).json({ no_access: "You cannot access this echo" }); + } catch (err) { + console.log(err); + res.status(500).json({ error: "Failed to fetch echo" }); + } +}); + + +/* ========================================================= + creating an echo + ========================================================= */ +router.post( + "/", + [authenticateJWT, upload.array("media", 10)], + async (req, res) => { + try { + const { + echo_name, + recipient_type, + text, + unlock_datetime, + show_sender_name, + lat, + lng, + customRecipients, + } = req.body; + const sender_id = req.user.id; + + // checking if any required fields are missing + if (!sender_id || !recipient_type || !unlock_datetime) { + return res.status(400).json({ error: "Missing required fields" }); + } + + // checking if unlock_datetime is in the future + const unlockTime = new Date(unlock_datetime); + + if (isNaN(unlockTime.getTime())) { + return res + .status(400) + .json({ error: "Invalid unlock_datetime format" }); + } + + if (unlockTime < new Date()) { + return res + .status(400) + .json({ error: "unlock_datetime must be in the future" }); + } + + // validating customRecipients array if recipient_type is 'custom' + if (recipient_type === "custom") { + if (!Array.isArray(customRecipients) || customRecipients.length === 0) { + return res.status(400).json({ + error: "Custom recipient list must be a non-empty array.", + }); + } + + // prevent sending to self + if (customRecipients.includes(sender_id)) { + return res + .status(400) + .json({ error: "Cannot send custom echoes to yourself." }); + } + } + function getFileType(file) { + const name = file.originalname.split(".")[1].toLowerCase(); + if ( + name === "jpg" || + name === "jpeg" || + name === "png" || + name === "heic" || + name === "heif" || + name === "gif" || + name === "webp" + ) { + return "image"; + } else if (name === "mp4" || name === "mov" || name === "webm") { + return "video"; + } else { + return "other"; + } + } + + for (let i = 0; i < req.files.length; i++) { + const file = req.files[i]; + if (file.size > Math.pow(10, 9)) { + return res.status(400).json({ + error: "File size exceeds limit of 1 GB, no file uploaded.", + }); + } + } + + // Validate file types + for (let i = 0; i < req.files.length; i++) { + const file = req.files[i]; + if (getFileType(file) === "other") { + return res.status(400).json({ error: "File type not supported!" }); + } + } + const image_data_list = []; + for (let i = 0; i < req.files.length; i++) { + const uuid = crypto.randomUUID(); + const parallelUpload = new Upload({ + client: s3, + params: { + Bucket: bucketName, + Key: uuid, + Body: req.files[i].buffer, + ContentType: req.files[i].mimetype, + }, + }); + parallelUpload.on("httpUploadProgress", (progress) => { + console.log(progress); + }); + await parallelUpload.done(); + image_data_list.push({ + uuid, + fileType: getFileType(req.files[i]), + size: req.files[i].size, + }); + } + + // creating new echo + const newEcho = await Echoes.create({ + echo_name, + user_id: sender_id, + recipient_type, + text, + unlock_datetime, + show_sender_name, + lat, + lng, + }); + for (let i = 0; i < image_data_list.length; i++) { + const image_data = image_data_list[i]; + await Media.create({ + uuid: image_data.uuid, + echo_id: newEcho.id, + type: image_data.fileType, + file_size: image_data.size, + }); + } + // add custom recipients if custom + if (recipient_type === "custom") { + const echoRecipients = customRecipients.map((recipient_id) => ({ + echo_id: newEcho.id, + recipient_id, + })); + + await Echo_recipients.bulkCreate(echoRecipients); + } + + res.status(201).json(newEcho); + } catch (err) { + console.log(err); + res.status(500).json({ error: "Failed to create echo" }); + } + }, +); + +// arching or unarchiving an echo +router.patch("/:id/archive", authenticateJWT, async (req, res) => { + try { + const user_id = req.user.id; + const echo = await Echoes.findByPk(req.params.id); + + if (!echo) { + return res.status(404).json({ error: "Echo not found" }); + } + + if (echo.user_id !== user_id) { + return res + .status(403) + .json({ error: "You are not the owner of this echo." }); + } + + // Toggle archived status + echo.is_archived = !echo.is_archived; + await echo.save(); + + return res.status(200).json({ + message: echo.is_archived ? "Echo archived" : "Echo unarchived", + echo, + }); + } catch (err) { + return res + .status(500) + .json({ error: "Failed to toggle echo archive status" }); + } +}); + +/* ========================================================= + PATCH /api/echoes/:id/unlock + Unlock logic (visibility + time gate) + ========================================================= */ +router.patch("/:id/unlock", authenticateJWT, async (req, res) => { + try { + const user_id = req.user.id; + const echo = await Echoes.findByPk(req.params.id); + if (!echo) return res.status(404).json({ error: "Echo not found" }); + + const isCreator = echo.user_id === user_id; + + // ---- visibility checks (mirror your GET /:id) ---- + if (echo.recipient_type === "self" && !isCreator) { + return res.status(403).json({ private: "This echo is private" }); + } + + if (echo.recipient_type === "friend" && !isCreator) { + const isFriend = await Friends.findOne({ + where: { + [Op.or]: [ + { user_id: echo.user_id, friend_id: user_id }, + { user_id, friend_id: echo.user_id } + ], + status: "accepted", + }, + }); + if (!isFriend) { + return res.status(403).json({ not_friend: "Friends only" }); + } + } + + if (echo.recipient_type === "custom" && !isCreator) { + const isRecipient = await Echo_recipients.findOne({ + where: { echo_id: echo.id, recipient_id: user_id }, + }); + if (!isRecipient) { + return res.status(403).json({ no_access: "Not a recipient" }); + } + } + + // ---- time gate ---- + const now = new Date(); + if (now < new Date(echo.unlock_datetime) && !isCreator) { + return res.status(403).json({ locked: "Unlock time not reached" }); + } + + // At this point, consider the echo "opened" by this user. + // If you later add a table for history/views, insert it here. + + const payload = { ...echo.toJSON(), client_unlocked: true }; + return res.json(payload); + } catch (err) { + console.error("Unlock error:", err); + res.status(500).json({ error: "Failed to unlock echo" }); + } +}); + +// deleting an echo +router.delete("/:id", authenticateJWT, async (req, res) => { + try { + const user_id = req.user.id; + const echo = await Echoes.findByPk(req.params.id, { include: Media }); + + // check if echo exists + if (!echo) { + return res.status(404).json({ error: "Echo not found." }); + } + + // check ownership + if (user_id !== echo.user_id) { + return res + .status(403) + .json({ error: "You are not the owner of this echo." }); + } + + const params = { + Bucket: bucketName, + Delete: { + Objects: echo.media.map((media) => ({ Key: media.uuid })), + }, + }; + const command = new DeleteObjectsCommand(params); + await s3.send(command); + + // delete the echo + await echo.destroy(); + + return res.status(200).json({ + message: "Echo deleted successfully", + id: echo.id, + }); + } catch (err) { + console.error(err); + return res.status(500).json({ error: "Error deleting this echo" }); + } +}); + +module.exports = router; diff --git a/api/friends.js b/api/friends.js new file mode 100644 index 0000000..d92b379 --- /dev/null +++ b/api/friends.js @@ -0,0 +1,194 @@ +const express = require("express") +const router = express.Router(); +const { Friends } = require("../database"); +const { authenticateJWT } = require("../auth"); +const { Op } = require("sequelize"); + +router.get("/", authenticateJWT, async (req, res) => { + try { + const userId = req.user.id; + const friends = await Friends.findAll({ + where: { + [Op.or]: [ + {user_id: userId}, + {friend_id: userId} + ], + status: { + [Op.or]: ["accepted", "pending", "blocked"] + } + } + }); + + res.json(friends); + } catch (err) { + res.status(500).json({ error: "Failed to fetch friends" }); + } +}); + +router.post("/", authenticateJWT, async (req, res) => { + try { + const user_id = req.user.id; + const { friend_id } = req.body; + + // cannot send friend requests to self + if (user_id === friend_id) { + return res.status(400).json({error: "Cannot send a friend request to self"}); + } + + // auto-accept if reverse pending request exists + const reverseRequest = await Friends.findOne({ + where: { user_id: friend_id, friend_id: user_id, status: "pending"} + }); + + if (reverseRequest) { + reverseRequest.status = "accepted"; + await reverseRequest.save(); + return res.status(200).json(reverseRequest); + } + + // checking if already friends + const isFriend = await Friends.findOne({ + where: { + [Op.or]: [ + {user_id, friend_id}, + {user_id: friend_id, friend_id: user_id} + ], + status: "accepted" + } + }); + + if (isFriend) { + return res.status(403).json({ message: "Already friends"}); + } + + // check if there's already an existing request + const requestExists = await Friends.findOne({ + where: { + [Op.or]: [ + {user_id, friend_id}, + {user_id: friend_id, friend_id: user_id} + ], + status: "pending" + } + }); + + if (requestExists) { + return res.status(403).json({message: "Friend request sent already"}); + } + + // create pending request + const newFriendRequest = await Friends.create({ user_id, friend_id}); + res.status(201).json(newFriendRequest); + } catch (err) { + console.log(err); + return res.status(500).json({error: "Failed to send friend request"}); + } +}); + +router.patch("/:id/accept", authenticateJWT, async (req, res) => { + try { + const userId = req.user.id; + const friendRequest = await Friends.findByPk(req.params.id); + + if (!friendRequest) { + return res.status(404).json({error: "Friend request not found."}); + } + + // Ensure current user is the receiver + if (friendRequest.friend_id !== userId) { + return res.status(403).json({ error: "You are not the recipient of this friend request." }); + } + + // check if already accepted + if (friendRequest.status === "accepted") { + return res.status(400).json({error: "This person is already your friend."}); + } + + // must be pending to accept + if (friendRequest.status !== "pending") { + return res.status(400).json({error: "This request is not pending."}); + } + + // Accept friend request + friendRequest.status = "accepted"; + await friendRequest.save(); + + return res.status(200).json({ + message: "Friend request accepted", + friendRequest + }); + + } catch (err) { + console.error(err); + return res.status(500).json({error: "Error accepting friend request."}); + } +}); + +router.patch("/:id/block", authenticateJWT, async (req, res) => { + try { + const userId = req.user.id; + const friendship = await Friends.findByPk(req.params.id); + + if (!friendship) { + return res.status(404).json({error: "Friendship not found."}); + } + + // Ensure current user is the receiver + if (friendship.friend_id !== userId) { + return res.status(403).json({ error: "You are not the recipient of this friend request." }); + } + + // Checking if user is blocked already + if (friendship.status === "blocked") { + return res.status(400).json({ error: "User already blocked." }) + } + + // Block user from sending friend requests + friendship.status = "blocked"; + await friendship.save(); + + return res.status(200).json({ + message: "User successfully blocked", + friendStatus: friendship + }); + + } catch (err) { + console.error(err); + return res.status(500).json({error: "Error blocking user."}); + } +}); + +router.delete("/:id", authenticateJWT, async (req, res) => { + try { + const userId = req.user.id; + const friendship = await Friends.findByPk(req.params.id); + + // friendship not found + if (!friendship) { + return res.status(404).json({error: "Friendship not found."}); + } + + // Ensure current user is in the friendship pair + if (friendship.friend_id !== userId && friendship.user_id !== userId) { + return res.status(403).json({ error: "You are not a part of this friendship." }); + } + + await friendship.destroy(); + + return res.status(200).json({ + message: "Friend or friend request removed successfully.", + friendship + }); + + } catch (err) { + console.error(err); + return res.status(500).json({error: "Error removing friend or friend request"}); + } +}); + +module.exports = router; + + + + + diff --git a/api/index.js b/api/index.js index f08162e..cd2e4f7 100644 --- a/api/index.js +++ b/api/index.js @@ -1,7 +1,19 @@ const express = require("express"); const router = express.Router(); const testDbRouter = require("./test-db"); +const usersRouter = require("./users") +const echoesRouter = require("./echoes"); +const friendsRouter = require("./friends"); +const repliesRouter = require("./replies"); +const reactionsRouter = require("./reactions"); +const tagsRouter = require("./tags"); router.use("/test-db", testDbRouter); - +router.use("/users", usersRouter); +router.use("/echoes", echoesRouter); +router.use("/friends", friendsRouter); +router.use("/replies", repliesRouter); +router.use("/reactions", reactionsRouter); +router.use("/tags", tagsRouter); module.exports = router; + diff --git a/api/reactions.js b/api/reactions.js new file mode 100644 index 0000000..871e7dd --- /dev/null +++ b/api/reactions.js @@ -0,0 +1,95 @@ +const express = require("express"); +const router = express.Router(); +const { Reactions, Echoes } = require("../database"); +const { authenticateJWT } = require("../auth"); +const { Op } = require("sequelize"); + +router.post("/echoes/:id", authenticateJWT, async(req, res) => { + try { + let { type } = req.body; + const userId = req.user.id; + const echoId = parseInt(req.params.id, 10); + + // validate echoId + if (isNaN(echoId)) { + return res.status(400).json({error: "Invalid echo Id."}); + } + + // validate reaction type + type = type?.trim(); + const allowedTypes = Reactions.getAttributes().type.values; + if (!type || !allowedTypes.includes(type)) { + return res.status(400).json({error: `Invalid reaction type. Allowed values: ${allowedTypes.join(', ')}`}); + } + + // check if echo exists + const echo = await Echoes.findByPk(echoId); + if (!echo) { + return res.status(404).json({error: "Echo not found."}); + } + + // check for existing reaction + const existingReaction = await Reactions.findOne({ + where: { echo_id: echoId, user_id: userId } + }); + if (existingReaction) { + if (existingReaction.type === type) { + await existingReaction.destroy(); + return res.status(200).json({ + message: "Reaction removed" + }); + } + existingReaction.type = type; + await existingReaction.save(); + return res.status(200).json({ + message: "Reaction updated", + reaction: existingReaction + }); + } + + // creating new reaction + const reaction = await Reactions.create({ + echo_id: echoId, + user_id: userId, + type + }); + + return res.status(201).json({ + message: "Reacted to echo.", + reaction + }); + + } catch (err) { + console.error(err); + return res.status(500).json({error: "Error adding reaction to echo."}); + } +}); + +router.get("/echoes/:id", async (req, res) => { + try { + const echoId = parseInt(req.params.id, 10); + + // validate echoId + if (isNaN(echoId)) { + return res.status(400).json({error: "Invalid echo ID."}); + } + + // check if echo exists + const echo = await Echoes.findByPk(echoId); + if (!echo) { + return res.status(404).json({error: "Echo not found."}); + } + + // find all echo reactions + const echoReactions = await Reactions.findAll({ + where: { echo_id: echoId } + }); + + return res.json(echoReactions); + } catch (err) { + console.error(err); + return res.status(500).json({error: "Error finding reactions for echo."}); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/api/replies.js b/api/replies.js new file mode 100644 index 0000000..664523a --- /dev/null +++ b/api/replies.js @@ -0,0 +1,107 @@ +const express = require("express"); +const router = express.Router(); +const { Replies, Echoes } = require("../database"); +const { authenticateJWT } = require("../auth"); +const { Op } = require("sequelize"); + +router.post("/echoes/:id", authenticateJWT, async(req, res) => { + try { + const userId = req.user.id; + const echoId = parseInt(req.params.id, 10); + const { parent_reply_id, message } = req.body; + + // check that echo exists + const echo = await Echoes.findByPk(echoId); + if (!echo) { + return res.status(404).json({error: "Echo not found."}); + } + + // message field required + if (!message) { + return res.status(400).json({error: "Message is required."}); + } + + // if parent reply is provided, ensure it belongs to the same echo + if (parent_reply_id) { + const parentReply = await Replies.findByPk(parent_reply_id); + if (!parentReply || parentReply.echo_id !== echoId) { + return res.status(400).json({error: "Invalid parent reply for this echo."}); + } + } + + const newReply = await Replies.create({ + echo_id: echoId, + user_id: userId, + parent_reply_id, + message + }); + + res.status(201).json({ + message: "Reply added successfully.", + newReply + }); + } catch (err) { + console.error(err); + return res.status(500).json({ error: "Error posting reply."}); + } +}); + +router.get("/echoes/:id", async (req, res) => { + try { + const echoId = parseInt(req.params.id, 10); + + const echoReplies = await Replies.findAll({ + where: { + echo_id: echoId + } + }); + + return res.json(echoReplies); + + } catch (err) { + console.error(err); + return res.status(500).json({error: "Error fetching echo replies."}); + } +}); + +router.delete("/:reply_id/echoes/:echo_id", authenticateJWT, async (req, res) => { + try { + const userId = req.user.id; + const echoId = parseInt(req.params.echo_id, 10); + const replyId = parseInt(req.params.reply_id, 10); + + // check if echo exists + const echo = await Echoes.findByPk(echoId); + if (!echo) { + return res.status(404).json({error: "Echo not found."}); + } + + // Find reply and ensure it belongs to the echo and user + const reply = await Replies.findOne({ + where: { + id: replyId, + echo_id: echoId, + user_id: userId + } + }) + + // check if reply exists + if (!reply) { + return res.status(404).json({error: "Reply not found or you are not authorized to delete it."}); + } + + // delete reply + await reply.destroy(); + + return res.status(200).json({ + message: "Reply deleted successfully.", + reply + }); + + } catch (err) { + console.error(err); + return res.status(500).json({error: "Error deleting reply."}); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/api/tags.js b/api/tags.js new file mode 100644 index 0000000..c8045d7 --- /dev/null +++ b/api/tags.js @@ -0,0 +1,46 @@ +const express = require("express"); +const router = express.Router(); +const { Tags } = require("../database"); +const { authenticateJWT } = require("../auth"); +const { Op } = require("sequelize"); + +router.get("/", async(req, res) => { + try { + const tags = await Tags.findAll(); + + // check if tags exist + if (!tags) { + return res.status(404).json({error: "No tags found."}); + } + + // if tags found but none created + if (tags.length === 0) { + return res.status(400).json({error: "No tags have been created."}); + } + + res.json(tags); + } catch (err) { + console.error(err); + return res.status(500).json({error: "Error finding all tags."}); + } +}); + +router.post("/", authenticateJWT, async(req, res) => { + try { + + } catch (err) { + console.error(err); + return res.status(500).json({error: "Error adding tag to echo."}); + } +}); + +router.get("/:id/echoes", async (req, res) => { + try { + + } catch (err) { + console.error(err); + return res.status(500).json({error: "Error finding echos with this tag."}); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/api/users.js b/api/users.js new file mode 100644 index 0000000..4bcf06f --- /dev/null +++ b/api/users.js @@ -0,0 +1,105 @@ +const express = require("express") +const router = express.Router(); +const { User, Echoes, Friends } = require("../database"); +const { Op } = require("sequelize"); +const { authenticateJWT } = require("../auth"); + +router.get("/", async (req, res) => { + try { + const users = await User.findAll({ + attributes: { exclude: ["password_hash"] } + }); + res.json(users); + } catch (err) { + res.status(500).json({ error: "Failed to fetch users" }); + } +}); +// GET /api/users/me – return the logged-in user +router.get("/me", authenticateJWT, async (req, res) => { + try { + const me = await User.findByPk(req.user.id, { + attributes: { exclude: ["password_hash"] }, + }); + if (!me) return res.status(404).json({ error: "User not found." }); + res.json(me); + } catch (e) { + console.error(e); + res.status(500).json({ error: "Failed to load current user." }); + } +}); + + +// PATCH /api/users/:id – update own profile +router.patch("/:id", authenticateJWT, async (req, res) => { + const id = parseInt(req.params.id, 10); + if (req.user.id !== id) { + return res.status(403).json({ error: "You can only edit your own profile." }); + } + + // Update only fields that exist in your model + // (Your User model has username, bio, avatar_url, email, etc. — not firstName/lastName/img) + const allowed = ["username", "bio", "avatar_url", "email"]; + const patch = {}; + for (const k of allowed) if (req.body[k] !== undefined) patch[k] = req.body[k]; + + try { + const user = await User.findByPk(id); + if (!user) return res.status(404).json({ error: "User not found." }); + await user.update(patch); + + res.json({ user }); + } catch (e) { + console.error(e); + res.status(500).json({ error: "Failed to update profile." }); + } +}); + +router.get("/:id", async (req, res) => { + try { + const user = await User.findByPk(req.params.id, { + attributes: { exclude: ["password_hash"] } + }); + + res.json(user); + } catch (err) { + res.status(500).json({ error: "Failed to fetch user" }) + } +}); +// GET /api/users/:id/friends – accepted friends of a user +router.get("/:id/friends", async (req, res) => { + const id = parseInt(req.params.id, 10); + try { + const rows = await Friends.findAll({ + where: { + [Op.or]: [{ user_id: id }, { friend_id: id }], + status: "accepted", + }, + }); + const otherIds = rows.map(r => (r.user_id === id ? r.friend_id : r.user_id)); + const list = otherIds.length + ? await User.findAll({ + where: { id: otherIds }, + attributes: { exclude: ["password_hash"] }, + }) + : []; + res.json(list); + } catch (e) { + console.error(e); + res.status(500).json({ error: "Failed to fetch user's friends." }); + } +}); + +router.get("/:id/echoes", async (req, res) => { + try { + const user_echoes = await Echoes.findAll({ + where: { + user_id: req.params.id + } + }); + res.status(200).json(user_echoes); + } catch (err) { + res.status(500).json({error: "Failed to fetch user echoes"}); + } +}); + +module.exports = router; diff --git a/auth/index.js b/auth/index.js index 07968c5..5cf1524 100644 --- a/auth/index.js +++ b/auth/index.js @@ -76,7 +76,7 @@ router.post("/auth0", async (req, res) => { email: user.email, }, JWT_SECRET, - { expiresIn: "24h" } + { expiresIn: "24h" }, ); res.cookie("token", token, { @@ -104,12 +104,12 @@ router.post("/auth0", async (req, res) => { // Signup route router.post("/signup", async (req, res) => { try { - const { username, password } = req.body; + const { username, password, email } = req.body; - if (!username || !password) { + if (!username || !password || !email) { return res .status(400) - .send({ error: "Username and password are required" }); + .send({ error: "Username, password and email are required" }); } if (password.length < 6) { @@ -126,7 +126,11 @@ router.post("/signup", async (req, res) => { // Create new user const passwordHash = User.hashPassword(password); - const user = await User.create({ username, passwordHash }); + const user = await User.create({ + username, + password_hash: passwordHash, + email, + }); // Generate JWT token const token = jwt.sign( @@ -137,7 +141,7 @@ router.post("/signup", async (req, res) => { email: user.email, }, JWT_SECRET, - { expiresIn: "24h" } + { expiresIn: "24h" }, ); res.cookie("token", token, { @@ -149,7 +153,7 @@ router.post("/signup", async (req, res) => { res.send({ message: "User created successfully", - user: { id: user.id, username: user.username }, + user: { id: user.id, username: user.username, email: username.email }, }); } catch (error) { console.error("Signup error:", error); @@ -188,7 +192,7 @@ router.post("/login", async (req, res) => { email: user.email, }, JWT_SECRET, - { expiresIn: "24h" } + { expiresIn: "24h" }, ); res.cookie("token", token, { diff --git a/auth/routes.js b/auth/routes.js new file mode 100644 index 0000000..e4095a3 --- /dev/null +++ b/auth/routes.js @@ -0,0 +1,112 @@ +const express = require('express'); +const jwt = require('jsonwebtoken'); +const passport = require('passport'); +const GoogleStrategy = require('passport-google-oauth20').Strategy; +const { User } = require("../database"); + + +const router = express.Router(); + +const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key"; + +// Passport serialization +passport.serializeUser((user, done) => { + done(null, user.id); +}); + +passport.deserializeUser(async (id, done) => { + try { + const user = await User.findByPk(id); + done(null, user); + } catch (error) { + done(error, null); + } +}); + +passport.use(new GoogleStrategy({ + clientID: process.env.GOOGLE_CLIENT_ID, + clientSecret: process.env.GOOGLE_CLIENT_SECRET, + callbackURL: process.env.GOOGLE_CALLBACK_URL + }, async (accessToken, refreshToken, profile, done) => { + try { + let user = await User.findOne({where: {googleId: profile.id}}); + + if (!user) { + // Try by email + const email = profile.emails?.[0]?.value; + if (email) { + user = await User.findOne({ where: { email } }); + + if (user) { + user.googleId = profile.id; + await user.save(); + } + } + } + + if (!user) { + // Create new user + const email = profile.emails?.[0]?.value || null; + const username = email ? email.split('@')[0] : `user_${Date.now()}`; + + // Ensure username is unique + let finalUsername = username; + let counter = 1; + while (await User.findOne({ where: { username: finalUsername } })) { + finalUsername = `${username}_${counter}`; + counter++; + } + + user = await User.create({ + googleId: profile.id, + email, + username: finalUsername, + passwordHash: null, + }); + } + + return done(null, user); + } catch (error) { + return done(error, null); + } + })); + + //OAuth flow + router.get("/google", passport.authenticate("google", { + scope: ["profile", "email"] + })); + + //Handle callback + router.get("/google/callback", passport.authenticate("google", {session: false}), + async (req,res) => { + try { + let user = req.user; + + // Generate JWT token + const token = jwt.sign( + { + id: user.id, + username: user.username, + auth0Id: user.auth0Id, + email: user.email, + }, + JWT_SECRET, + { expiresIn: "24h" } + ); + + res.cookie("token", token, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "strict", + maxAge: 24 * 60 * 60 * 1000, // 24 hours + }); + + res.redirect(`${process.env.FRONTEND_URL}/dashboard`); + } catch (error) { + res.redirect(`${process.env.FRONTEND_URL}/login?error=google&message=Google%20login%20failed`); + + } + + }); + + module.exports = router; \ No newline at end of file diff --git a/database/db.js b/database/db.js index b251a9d..3b580f1 100644 --- a/database/db.js +++ b/database/db.js @@ -3,7 +3,7 @@ const { Sequelize } = require("sequelize"); const pg = require("pg"); // Feel free to rename the database to whatever you want! -const dbName = "capstone-1"; +const dbName = "capstone-2"; const db = new Sequelize( process.env.DATABASE_URL || `postgres://localhost:5432/${dbName}`, diff --git a/database/echo_recipients.js b/database/echo_recipients.js new file mode 100644 index 0000000..2e3223e --- /dev/null +++ b/database/echo_recipients.js @@ -0,0 +1,29 @@ +const { DataTypes } = require("sequelize"); +const db = require("./db"); + +const Echo_recipients = db.define("echo_recipients", { + echo_id: { + type: DataTypes.INTEGER, + primaryKey: true, + allowNull: false, + references: { + model: 'echoes', // target table name + key: 'id' + }, + }, + + recipient_id: { + type: DataTypes.INTEGER, + primaryKey: true, + allowNull: false, + references: { + model: 'users', // target table name + key: 'id' + }, + } +}, { + tableName: 'echo_recipients', + timestamps: false +}); + +module.exports = Echo_recipients; diff --git a/database/echo_tags.js b/database/echo_tags.js new file mode 100644 index 0000000..f018005 --- /dev/null +++ b/database/echo_tags.js @@ -0,0 +1,29 @@ +const { DataTypes } = require("sequelize"); +const db = require("./db"); + +const Echo_tags = db.define('echo_tags', { + echo_id: { + type: DataTypes.INTEGER, + allowNull: false, + primaryKey: true, + references: { + model: "echoes", // target table + key: "id" + } + }, + + tag_id: { + type: DataTypes.INTEGER, + allowNull: false, + primaryKey: true, + references: { + model: "tags", + key: "id" + } + } +}, { + tableName: 'echo_tags', + timestamps: false +}); + +module.exports = Echo_tags; diff --git a/database/echoes.js b/database/echoes.js new file mode 100644 index 0000000..9a3650c --- /dev/null +++ b/database/echoes.js @@ -0,0 +1,90 @@ +const { DataTypes } = require("sequelize"); +const db = require("./db"); + +const Echoes = db.define( + "echoes", + { + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true, + }, + + echo_name: { + type: DataTypes.TEXT, + allowNull: true, + }, + + user_id: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: "users", // target table + key: "id", + }, + }, + + recipient_type: { + type: DataTypes.ENUM("self", "friend", "public", "custom"), + allowNull: false, + }, + + text: { + type: DataTypes.TEXT, + allowNull: true, + }, + + unlock_datetime: { + type: DataTypes.DATE, + allowNull: false, + }, + + is_unlocked: { + type: DataTypes.BOOLEAN, + defaultValue: false, + allowNull: false, + }, + + is_archived: { + type: DataTypes.BOOLEAN, + defaultValue: false, + allowNull: false, + }, + + is_saved: { + type: DataTypes.BOOLEAN, + defaultValue: false, + allowNull: false, + }, + + show_sender_name: { + type: DataTypes.BOOLEAN, + defaultValue: true, + allowNull: false, + }, + + location_locked: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: false, + }, + + lat: { + type: DataTypes.DECIMAL(9, 6), + allowNull: true, + }, + + lng: { + type: DataTypes.DECIMAL(9, 6), + allowNull: true, + }, + }, + { + tableName: "echoes", + timestamps: true, + createdAt: "created_at", + updatedAt: "updated_at", + }, +); + +module.exports = Echoes; diff --git a/database/friends.js b/database/friends.js new file mode 100644 index 0000000..6ce2209 --- /dev/null +++ b/database/friends.js @@ -0,0 +1,48 @@ +const { DataTypes } = require("sequelize"); +const db = require("./db"); + +const Friends = db.define("friends", { + id: { + primaryKey: true, + type: DataTypes.INTEGER, + autoIncrement: true + }, + + user_id: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: 'users', + key: 'id' + } + }, + + friend_id: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: 'users', + key: 'id' + } + }, + + status: { + type: DataTypes.ENUM('pending', 'accepted', 'blocked'), + allowNull: false, + defaultValue: 'pending' + } +}, { + tableName: 'friends', + timestamps: true, + createdAt: 'created_at', + updatedAt: 'updated_at', + indexes: [ + { + unique: true, + fields: ['user_id', 'friend_id'], + name: 'unique_user_friend_pair' + } + ] +}); + +module.exports = Friends; \ No newline at end of file diff --git a/database/index.js b/database/index.js index e498df6..211158b 100644 --- a/database/index.js +++ b/database/index.js @@ -1,7 +1,208 @@ const db = require("./db"); const User = require("./user"); +const Echoes = require('./echoes'); +const Echo_recipients = require('./echo_recipients'); +const Echo_tags = require('./echo_tags'); +const Friends = require('./friends'); +const Media = require('./media'); +const Reactions = require('./reactions'); +const Replies = require('./replies'); +const Reports = require('./reports'); +const Tags = require('./tags'); + +//--------Defining associations------------- + +// Echoes <> user (sender) +Echoes.belongsTo(User, { + foreignKey: 'user_id', + as: 'sender', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' +}); +User.hasMany(Echoes, { + foreignKey: 'user_id', + as: 'sent_echoes', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' +}); + +// Echoes <> media +Echoes.hasMany(Media, { + foreignKey: 'echo_id', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' +}); +Media.belongsTo(Echoes, { + foreignKey: 'echo_id', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' +}); + +// Replies <> Echoes +Replies.belongsTo(Echoes, { + foreignKey: 'echo_id', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' +}); +Echoes.hasMany(Replies, { + foreignKey: 'echo_id', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' +}); + +// Replies <> User +Replies.belongsTo(User, { + foreignKey: 'user_id', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' +}); +User.hasMany(Replies, { + foreignKey: 'user_id', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' +}); + +// Replies <> Replies (nested replies) +Replies.belongsTo(Replies, { + foreignKey: 'parent_reply_id', + as: 'parent_reply', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' +}); +Replies.hasMany(Replies, { + foreignKey: 'parent_reply_id', + as: 'child_replies', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' +}); + +// Media <> Replies +Media.belongsTo(Replies, { + foreignKey: 'reply_id', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' +}); +Replies.hasMany(Media, { + foreignKey: 'reply_id', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' +}); + +// Reactions <> Echoes +Reactions.belongsTo(Echoes, { + foreignKey: 'echo_id', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' +}); +Echoes.hasMany(Reactions, { + foreignKey: 'echo_id', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' +}); + +// Reactions <> User +Reactions.belongsTo(User, { + foreignKey: 'user_id', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' +}); +User.hasMany(Reactions, { + foreignKey: 'user_id', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' +}); + +// Reports <> Echoes +Reports.belongsTo(Echoes, { + foreignKey: 'echo_id', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' +}); +Echoes.hasMany(Reports, { + foreignKey: 'echo_id', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' +}); + +// Reports <> Users +Reports.belongsTo(User, { + foreignKey: 'user_id', + as: 'reporter', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' +}); +User.hasMany(Reports, { + foreignKey: 'user_id', + as: 'submitted_reports', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' +}); + + +// Echoes <> Tags (many to many through echo_tags) +Echoes.belongsToMany(Tags, { + through: Echo_tags, + foreignKey: 'echo_id', + otherKey: 'tag_id', + as: 'tags', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' +}); +Tags.belongsToMany(Echoes, { + through: Echo_tags, + foreignKey: 'tag_id', + otherKey: 'echo_id', + as: 'echoes', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' +}); + +// Echoes <> users (recipients- many to many) +Echoes.belongsToMany(User, { + through: Echo_recipients, + foreignKey: 'echo_id', + otherKey: 'recipient_id', + as: 'recipients', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' +}); +User.belongsToMany(Echoes, { + through: Echo_recipients, + foreignKey: 'recipient_id', + otherKey: 'echo_id', + as: 'received_echoes', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' +}); + +// Users <> Users (friends) +User.belongsToMany(User, { + through: Friends, + as: 'outgoingFriends', // users I added + foreignKey: 'user_id', + otherKey: 'friend_id', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' +}); +User.belongsToMany(User, { + through: Friends, + as: 'incomingFriends', // user who added me + foreignKey: 'friend_id', + otherKey: 'user_id', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' +}); module.exports = { db, User, + Echoes, + Echo_recipients, + Echo_tags, + Friends, + Media, + Reactions, + Replies, + Reports, + Tags }; diff --git a/database/media.js b/database/media.js new file mode 100644 index 0000000..f3b2734 --- /dev/null +++ b/database/media.js @@ -0,0 +1,75 @@ +const { DataTypes } = require("sequelize"); +const db = require("./db"); + +const Media = db.define( + "media", + { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + }, + + echo_id: { + type: DataTypes.INTEGER, + allowNull: true, + references: { + model: "echoes", + key: "id", + }, + }, + + reply_id: { + type: DataTypes.INTEGER, + allowNull: true, + references: { + model: "replies", + key: "id", + }, + }, + + type: { + type: DataTypes.ENUM("image", "audio", "video"), + allowNull: false, + }, + + uuid: { + type: DataTypes.UUID, + allowNull: false, + }, + + file_size: { + type: DataTypes.BIGINT, + allowNull: false, + }, + + duration_seconds: { + // only applies to audio or video + type: DataTypes.INTEGER, + allowNull: true, + }, + + signed_url: { + type: DataTypes.TEXT("long"), + allowNull: true, + defaultValue: "", + }, + }, + { + timestamps: true, + createdAt: "created_at", + updatedAt: "updated_at", + validate: { + eitherEchoOrReply() { + if (!this.echo_id && !this.reply_id) { + throw new Error("Media must belong to either an echo or reply."); + } + if (this.echo_id && this.reply_id) { + throw new Error("Media cannot belong to both an echo and a reply."); + } + }, + }, + }, +); + +module.exports = Media; diff --git a/database/reactions.js b/database/reactions.js new file mode 100644 index 0000000..3c2e62a --- /dev/null +++ b/database/reactions.js @@ -0,0 +1,44 @@ +const { DataTypes } = require('sequelize'); +const db = require('./db'); + +const Reactions = db.define('reactions', { + id: { + primaryKey: true, + type: DataTypes.INTEGER, + autoIncrement: true + }, + + echo_id: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: 'echoes', + key: 'id' + } + }, + + user_id: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: 'users', + key: 'id' + } + }, + + type: { + type: DataTypes.ENUM('sad', 'funny', 'happy', 'like', 'love', 'angry', 'wow'), + allowNull: false + } +}, { + timestamps: true, + createdAt: 'created_at', + updatedAt: 'updated_at', + indexes: [{ + unique: true, + fields: ['user_id', 'echo_id'], + name: 'unique_user_echo_reaction_pair' + }] +}); + +module.exports = Reactions; diff --git a/database/replies.js b/database/replies.js new file mode 100644 index 0000000..e19885c --- /dev/null +++ b/database/replies.js @@ -0,0 +1,49 @@ +const { DataTypes } = require('sequelize'); +const db = require('./db'); + +const Replies = db.define('replies', { + id: { + primaryKey: true, + autoIncrement: true, + type: DataTypes.INTEGER + }, + + echo_id: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: 'echoes', + key: 'id' + } + }, + + user_id: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: 'users', + key: 'id' + } + }, + + parent_reply_id: { + type: DataTypes.INTEGER, + allowNull: true, + references: { + model: 'replies', + key: 'id' + } + }, + + message: { + type: DataTypes.TEXT, + allowNull: false, + } +}, { + tableName: 'replies', + timestamps: true, + createdAt: 'created_at', + updatedAt: 'updated_at' +}); + +module.exports = Replies; \ No newline at end of file diff --git a/database/reports.js b/database/reports.js new file mode 100644 index 0000000..d83737f --- /dev/null +++ b/database/reports.js @@ -0,0 +1,56 @@ +const { DataTypes } = require("sequelize"); +const db = require("./db"); + +const Reports = db.define("reports", { + id: { + primaryKey: true, + type: DataTypes.INTEGER, + autoIncrement: true + }, + + echo_id: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: 'echoes', + key: 'id' + } + }, + + reporter_id: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: 'users', + key: 'id' + } + }, + + type: { + type: DataTypes.ENUM('spam', 'harrassment', 'hate_speech', 'explicit', 'other'), + allowNull: false, + }, + + reasons: { + // option message for report + type: DataTypes.TEXT, + allowNull: true + }, + + status: { + type: DataTypes.ENUM('pending', 'reviewed', 'dismissed'), + allowNull: false, + defaultValue: 'pending' + }, + + reviewed_at: { + type: DataTypes.DATE, + allowNull: true + } +}, { + timestamps: true, + createdAt: 'created_at', + updatedAt: 'updated_at', +}); + +module.exports = Reports; \ No newline at end of file diff --git a/database/seed.js b/database/seed.js index e58b595..13abd77 100644 --- a/database/seed.js +++ b/database/seed.js @@ -1,5 +1,6 @@ const db = require("./db"); -const { User } = require("./index"); +const { User, Echoes, Friends, Echo_recipients, Replies, Reactions, Tags } = require("./index"); + const seed = async () => { try { @@ -7,17 +8,245 @@ const seed = async () => { await db.sync({ force: true }); // Drop and recreate tables const users = await User.bulkCreate([ - { username: "admin", passwordHash: User.hashPassword("admin123") }, - { username: "user1", passwordHash: User.hashPassword("user111") }, - { username: "user2", passwordHash: User.hashPassword("user222") }, + { username: "jeramy", password_hash: User.hashPassword("123456"), email: "jeramy@gmail.com" }, + { username: "aiyanna", password_hash: User.hashPassword("456789"), email: "aiyanna@gmail.com" }, + { username: "emmanuel", password_hash: User.hashPassword("789101"), email: "emmanuel@gmail.com" }, + { username: "olivia", password_hash: User.hashPassword("101112"), email: "olivia@gmail.com" }, ]); console.log(`👤 Created ${users.length} users`); - // Create more seed data here once you've created your models - // Seed files are a great way to test your database schema! + const echoes = await Echoes.bulkCreate([ + { + echo_name: "Jeramy's Private Echo", + user_id: users[0].id, // Jeramy + recipient_type: "self", + text: "This is Jeramy's private echo", + unlock_datetime: new Date(Date.now() + 1000 * 60 * 60), // unlock in 1 hour + show_sender_name: true, + is_saved: false, // default + is_archived: false, // default + location_locked: false, // default + lat: null, + lng: null + }, + { + echo_name: "Aiyanna's Friends Echo", + user_id: users[1].id, // Aiyanna + recipient_type: "friend", + text: "Aiyanna's echo to friends", + unlock_datetime: new Date(Date.now() - 1000 * 60 * 60), // already unlocked + show_sender_name: false, + is_saved: false, + is_archived: false, + location_locked: false, + lat: null, + lng: null + }, + { + echo_name: "Public Echo by Emmanuel", + user_id: users[2].id, // Emmanuel + recipient_type: "public", + text: "Public echo by Emmanuel for everyone to see", + unlock_datetime: new Date(), + is_archived: true, + is_saved: false, + show_sender_name: true, + location_locked: false, + lat: null, + lng: null + }, + { + echo_name: "Olivia's Public Echo", + user_id: users[3].id, // Olivia + recipient_type: "public", + text: "Olivia's echo is for everyone!", + unlock_datetime: new Date(Date.now() + 1000 * 60 * 60 * 24), // unlock in 1 day + show_sender_name: true, + is_saved: false, + is_archived: false, + location_locked: false, + lat: null, + lng: null + }, + { + echo_name: "Jeramy's Custom Echo", + user_id: users[0].id, // Jeramy + recipient_type: "custom", + text: "Jeramy's custom echo wwere only specific users can see it", + unlock_datetime: new Date(Date.now() + 1000 * 60 * 45), // unlock in 45 minutes + show_sender_name: true, + is_saved: false, + is_archived: false, + location_locked: false, + lat: null, + lng: null + }, + { + echo_name: "Walk by Saint Lawrence", + user_id: users[0].id, // Jeramy + recipient_type: "friend", + text: "Remember when we first passed Saint Lawrence Triangle?", + unlock_datetime: new Date("2025-09-01T12:00:00Z"), + show_sender_name: true, + location_locked: true, + lat: 40.8365, + lng: -73.8677, + tags: ["memory", "friendship"] + }, + { + echo_name: "Lunch under the tracks", + user_id: users[1].id, // Aiyanna + recipient_type: "public", + text: "Grab a lunch by the 6 train stop.", + unlock_datetime: new Date("2025-09-05T09:00:00Z"), + show_sender_name: false, + location_locked: true, + lat: 40.831589, + lng: -73.866882, + tags: ["food", "bronx"] + }, + { + echo_name: "Morning jog start", + user_id: users[2].id, // Emmanuel + recipient_type: "self", + text: "Started my day running through Saint Lawrence Triangle.", + unlock_datetime: new Date("2025-08-25T06:30:00Z"), + show_sender_name: true, + location_locked: true, + lat: 40.8350, + lng: -73.8680, + is_unlocked: true, + tags: ["fitness", "morning"] + }, + { + echo_name: "BX Park Bench Memory", + user_id: users[3].id, // Olivia + recipient_type: "public", + text: "Meet me by the park bench. You’ll know it when you see it.", + unlock_datetime: new Date(Date.now() + 1000 * 60 * 120), // +2 hours + show_sender_name: true, + location_locked: true, + lat: 40.82965, + lng: -73.86790, + tags: ["bronx", "park", "memory"] + }, + { + echo_name: "Soundview Corner Joke", + user_id: users[3].id, // Olivia + recipient_type: "friend", + text: "This corner still cracks me up 😂", + unlock_datetime: new Date(Date.now() - 1000 * 60 * 60), // already unlocked + show_sender_name: true, + location_locked: true, + lat: 40.82920, + lng: -73.86730, + tags: ["friends", "inside-joke"] + }, + { + echo_name: "Triangle Note for Olivia", + user_id: users[0].id, // Jeramy + recipient_type: "custom", + text: "For Olivia only — look up!", + unlock_datetime: new Date(Date.now() + 1000 * 60 * 30), // +30 min + show_sender_name: true, + location_locked: true, + lat: 40.82990, + lng: -73.86820, + tags: ["custom", "triangle"] + }, + { + echo_name: "Lunch Cart by the School", + user_id: users[1].id, // Aiyanna + recipient_type: "public", + text: "Best empanadas here at noon 🍽️", + unlock_datetime: new Date(Date.now() + 1000 * 60 * 60 * 24), // +1 day + show_sender_name: false, + location_locked: true, + lat: 40.82890, + lng: -73.86690, + tags: ["food", "lunch"] + } +]); + + + console.log(`📣 Created ${echoes.length} echoes`); + + const friends = await Friends.bulkCreate([ + { user_id: users[0].id, friend_id: users[1].id, status: "accepted" }, + { user_id: users[1].id, friend_id: users[0].id, status: "accepted" }, + { user_id: users[0].id, friend_id: users[2].id, status: "pending" }, + { user_id: users[2].id, friend_id: users[3].id, status: "blocked" }, + ]); + + + console.log(`🤝 Created ${friends.length} friendships`) + + const echoRecipients = await Echo_recipients.bulkCreate([ + { + echo_id: echoes[4].id, // Custom echo by Jeramy + recipient_id: users[1].id // Aiyanna + }, + { + echo_id: echoes[4].id, + recipient_id: users[2].id // Emmanuel + } + ]); + + console.log(`📨 Created ${echoRecipients.length} echo_recipients for custom echo`); + + // replies + const replies = await Replies.bulkCreate([ + { echo_id: echoes[0].id, user_id: users[1].id, message: "Hey Jeramy, nice private echo!" }, + { echo_id: echoes[0].id, user_id: users[2].id, message: "Agreed, this is cool." }, + { echo_id: echoes[1].id, user_id: users[0].id, message: "Glad to be your friend, Aiyanna!" }, + { echo_id: echoes[2].id, user_id: users[3].id, message: "Public echoes are great!" }, + // Nested reply (replying to the first reply above) + { echo_id: echoes[0].id, user_id: users[0].id, parent_reply_id: 1, message: "Thanks, Aiyanna!" } + ]); + console.log(`💬 Created ${replies.length} replies`); + + // reactions + const reactions = await Reactions.bulkCreate([ + // Echo 0 (self) — only Jeramy + { echo_id: echoes[0].id, user_id: users[0].id, type: 'love' }, + + // Echo 1 (friend, unlocked) — creator (Aiyanna) + Jeramy (friend accepted) + { echo_id: echoes[1].id, user_id: users[1].id, type: 'happy' }, + { echo_id: echoes[1].id, user_id: users[0].id, type: 'like' }, + + // Echo 2 (public, unlocked) — everyone can react + { echo_id: echoes[2].id, user_id: users[2].id, type: 'like' }, // creator + { echo_id: echoes[2].id, user_id: users[0].id, type: 'wow' }, + { echo_id: echoes[2].id, user_id: users[1].id, type: 'love' }, + { echo_id: echoes[2].id, user_id: users[3].id, type: 'funny' }, + + // Echo 3 (public, locked) — only Olivia + { echo_id: echoes[3].id, user_id: users[3].id, type: 'angry' }, + + // Echo 4 (custom, locked) — only Jeramy (creator) and recipients (u1, u2) + { echo_id: echoes[4].id, user_id: users[0].id, type: 'happy' }, // creator + { echo_id: echoes[4].id, user_id: users[1].id, type: 'wow' }, // recipient + { echo_id: echoes[4].id, user_id: users[2].id, type: 'like' } // recipient + ]); + + console.log(`😎 Created ${reactions.length} reactions`); + + const tags = await Tags.bulkCreate([ + { name: 'funny' }, + { name: 'inspirational' }, + { name: 'personal' }, + { name: 'friends' }, + { name: 'public' }, + { name: 'locked' }, + { name: 'announcement' }, + { name: 'random' } + ]); + + console.log(`🏷️ Created ${tags.length} tags`); + + console.log("🌱 Seeded the database"); - console.log("🌱 Seeded the database"); } catch (error) { console.error("Error seeding database:", error); if (error.message.includes("does not exist")) { diff --git a/database/tags.js b/database/tags.js new file mode 100644 index 0000000..93e9352 --- /dev/null +++ b/database/tags.js @@ -0,0 +1,21 @@ +const { DataTypes } = require("sequelize"); +const db = require("./db"); + +const Tags = db.define("tags", { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + allowNull: false, + autoIncrement: true, // auto generated tags IDs + }, + + name: { + type: DataTypes.TEXT, + allowNull: false, + } +}, { + tableName: 'tags', + timestamps: true +}); + +module.exports = Tags; diff --git a/database/user.js b/database/user.js index 755c757..2bf92ad 100644 --- a/database/user.js +++ b/database/user.js @@ -3,39 +3,61 @@ const db = require("./db"); const bcrypt = require("bcrypt"); const User = db.define("user", { + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true, + }, + username: { type: DataTypes.STRING, allowNull: false, - unique: true, - validate: { - len: [3, 20], - }, + unique: true + }, + + bio: { + type: DataTypes.TEXT, + allowNull: true }, + + avatar_url: { + type: DataTypes.ENUM("image", "gif"), + allowNull: true, + }, + email: { type: DataTypes.STRING, - allowNull: true, + allowNull: false, unique: true, validate: { isEmail: true, }, }, + auth0Id: { type: DataTypes.STRING, allowNull: true, unique: true, }, - passwordHash: { + + password_hash: { type: DataTypes.STRING, allowNull: true, - }, + comment: "Securely hashed password", + }, +},{ + tableName: 'users', + timestamps: true, + createdAt: 'created_at', + updatedAt: 'updated_at', }); // Instance method to check password User.prototype.checkPassword = function (password) { - if (!this.passwordHash) { + if (!this.password_hash) { return false; // Auth0 users don't have passwords } - return bcrypt.compareSync(password, this.passwordHash); + return bcrypt.compareSync(password, this.password_hash); }; // Class method to hash password diff --git a/package-lock.json b/package-lock.json index af0cf82..0f74a0b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,17 @@ { - "name": "capstone-i-backend", + "name": "capstone-1-backend", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "capstone-i-backend", + "name": "capstone-1-backend", "version": "1.0.0", "license": "ISC", "dependencies": { + "@aws-sdk/client-s3": "^3.864.0", + "@aws-sdk/lib-storage": "^3.864.0", + "@aws-sdk/s3-request-presigner": "^3.864.0", "bcrypt": "^6.0.0", "cookie-parser": "^1.4.7", "cors": "^2.8.5", @@ -16,6 +19,7 @@ "express": "^5.1.0", "jsonwebtoken": "^9.0.2", "morgan": "^1.10.0", + "multer": "^2.0.2", "pg": "^8.16.2", "sequelize": "^6.37.7" }, @@ -26,6 +30,1680 @@ "win-node-env": "^0.6.1" } }, + "node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/crc32c": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz", + "integrity": "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-5.2.0.tgz", + "integrity": "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@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/sha256-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/sha256-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/sha256-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-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "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/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util/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/util/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/util/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-sdk/client-s3": { + "version": "3.864.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.864.0.tgz", + "integrity": "sha512-QGYi9bWliewxumsvbJLLyx9WC0a4DP4F+utygBcq0zwPxaM0xDfBspQvP1dsepi7mW5aAjZmJ2+Xb7X0EhzJ/g==", + "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.864.0", + "@aws-sdk/credential-provider-node": "3.864.0", + "@aws-sdk/middleware-bucket-endpoint": "3.862.0", + "@aws-sdk/middleware-expect-continue": "3.862.0", + "@aws-sdk/middleware-flexible-checksums": "3.864.0", + "@aws-sdk/middleware-host-header": "3.862.0", + "@aws-sdk/middleware-location-constraint": "3.862.0", + "@aws-sdk/middleware-logger": "3.862.0", + "@aws-sdk/middleware-recursion-detection": "3.862.0", + "@aws-sdk/middleware-sdk-s3": "3.864.0", + "@aws-sdk/middleware-ssec": "3.862.0", + "@aws-sdk/middleware-user-agent": "3.864.0", + "@aws-sdk/region-config-resolver": "3.862.0", + "@aws-sdk/signature-v4-multi-region": "3.864.0", + "@aws-sdk/types": "3.862.0", + "@aws-sdk/util-endpoints": "3.862.0", + "@aws-sdk/util-user-agent-browser": "3.862.0", + "@aws-sdk/util-user-agent-node": "3.864.0", + "@aws-sdk/xml-builder": "3.862.0", + "@smithy/config-resolver": "^4.1.5", + "@smithy/core": "^3.8.0", + "@smithy/eventstream-serde-browser": "^4.0.5", + "@smithy/eventstream-serde-config-resolver": "^4.1.3", + "@smithy/eventstream-serde-node": "^4.0.5", + "@smithy/fetch-http-handler": "^5.1.1", + "@smithy/hash-blob-browser": "^4.0.5", + "@smithy/hash-node": "^4.0.5", + "@smithy/hash-stream-node": "^4.0.5", + "@smithy/invalid-dependency": "^4.0.5", + "@smithy/md5-js": "^4.0.5", + "@smithy/middleware-content-length": "^4.0.5", + "@smithy/middleware-endpoint": "^4.1.18", + "@smithy/middleware-retry": "^4.1.19", + "@smithy/middleware-serde": "^4.0.9", + "@smithy/middleware-stack": "^4.0.5", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/node-http-handler": "^4.1.1", + "@smithy/protocol-http": "^5.1.3", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.26", + "@smithy/util-defaults-mode-node": "^4.0.26", + "@smithy/util-endpoints": "^3.0.7", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-retry": "^4.0.7", + "@smithy/util-stream": "^4.2.4", + "@smithy/util-utf8": "^4.0.0", + "@smithy/util-waiter": "^4.0.7", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@aws-sdk/client-sso": { + "version": "3.864.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.864.0.tgz", + "integrity": "sha512-THiOp0OpQROEKZ6IdDCDNNh3qnNn/kFFaTSOiugDpgcE5QdsOxh1/RXq7LmHpTJum3cmnFf8jG59PHcz9Tjnlw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.864.0", + "@aws-sdk/middleware-host-header": "3.862.0", + "@aws-sdk/middleware-logger": "3.862.0", + "@aws-sdk/middleware-recursion-detection": "3.862.0", + "@aws-sdk/middleware-user-agent": "3.864.0", + "@aws-sdk/region-config-resolver": "3.862.0", + "@aws-sdk/types": "3.862.0", + "@aws-sdk/util-endpoints": "3.862.0", + "@aws-sdk/util-user-agent-browser": "3.862.0", + "@aws-sdk/util-user-agent-node": "3.864.0", + "@smithy/config-resolver": "^4.1.5", + "@smithy/core": "^3.8.0", + "@smithy/fetch-http-handler": "^5.1.1", + "@smithy/hash-node": "^4.0.5", + "@smithy/invalid-dependency": "^4.0.5", + "@smithy/middleware-content-length": "^4.0.5", + "@smithy/middleware-endpoint": "^4.1.18", + "@smithy/middleware-retry": "^4.1.19", + "@smithy/middleware-serde": "^4.0.9", + "@smithy/middleware-stack": "^4.0.5", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/node-http-handler": "^4.1.1", + "@smithy/protocol-http": "^5.1.3", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.26", + "@smithy/util-defaults-mode-node": "^4.0.26", + "@smithy/util-endpoints": "^3.0.7", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-retry": "^4.0.7", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.864.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.864.0.tgz", + "integrity": "sha512-LFUREbobleHEln+Zf7IG83lAZwvHZG0stI7UU0CtwyuhQy5Yx0rKksHNOCmlM7MpTEbSCfntEhYi3jUaY5e5lg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@aws-sdk/xml-builder": "3.862.0", + "@smithy/core": "^3.8.0", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/property-provider": "^4.0.5", + "@smithy/protocol-http": "^5.1.3", + "@smithy/signature-v4": "^5.1.3", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-utf8": "^4.0.0", + "fast-xml-parser": "5.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.864.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.864.0.tgz", + "integrity": "sha512-StJPOI2Rt8UE6lYjXUpg6tqSZaM72xg46ljPg8kIevtBAAfdtq9K20qT/kSliWGIBocMFAv0g2mC0hAa+ECyvg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.864.0", + "@aws-sdk/types": "3.862.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.864.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.864.0.tgz", + "integrity": "sha512-E/RFVxGTuGnuD+9pFPH2j4l6HvrXzPhmpL8H8nOoJUosjx7d4v93GJMbbl1v/fkDLqW9qN4Jx2cI6PAjohA6OA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.864.0", + "@aws-sdk/types": "3.862.0", + "@smithy/fetch-http-handler": "^5.1.1", + "@smithy/node-http-handler": "^4.1.1", + "@smithy/property-provider": "^4.0.5", + "@smithy/protocol-http": "^5.1.3", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "@smithy/util-stream": "^4.2.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.864.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.864.0.tgz", + "integrity": "sha512-PlxrijguR1gxyPd5EYam6OfWLarj2MJGf07DvCx9MAuQkw77HBnsu6+XbV8fQriFuoJVTBLn9ROhMr/ROAYfUg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.864.0", + "@aws-sdk/credential-provider-env": "3.864.0", + "@aws-sdk/credential-provider-http": "3.864.0", + "@aws-sdk/credential-provider-process": "3.864.0", + "@aws-sdk/credential-provider-sso": "3.864.0", + "@aws-sdk/credential-provider-web-identity": "3.864.0", + "@aws-sdk/nested-clients": "3.864.0", + "@aws-sdk/types": "3.862.0", + "@smithy/credential-provider-imds": "^4.0.7", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.864.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.864.0.tgz", + "integrity": "sha512-2BEymFeXURS+4jE9tP3vahPwbYRl0/1MVaFZcijj6pq+nf5EPGvkFillbdBRdc98ZI2NedZgSKu3gfZXgYdUhQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.864.0", + "@aws-sdk/credential-provider-http": "3.864.0", + "@aws-sdk/credential-provider-ini": "3.864.0", + "@aws-sdk/credential-provider-process": "3.864.0", + "@aws-sdk/credential-provider-sso": "3.864.0", + "@aws-sdk/credential-provider-web-identity": "3.864.0", + "@aws-sdk/types": "3.862.0", + "@smithy/credential-provider-imds": "^4.0.7", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.864.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.864.0.tgz", + "integrity": "sha512-Zxnn1hxhq7EOqXhVYgkF4rI9MnaO3+6bSg/tErnBQ3F8kDpA7CFU24G1YxwaJXp2X4aX3LwthefmSJHwcVP/2g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.864.0", + "@aws-sdk/types": "3.862.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.864.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.864.0.tgz", + "integrity": "sha512-UPyPNQbxDwHVGmgWdGg9/9yvzuedRQVF5jtMkmP565YX9pKZ8wYAcXhcYdNPWFvH0GYdB0crKOmvib+bmCuwkw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.864.0", + "@aws-sdk/core": "3.864.0", + "@aws-sdk/token-providers": "3.864.0", + "@aws-sdk/types": "3.862.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.864.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.864.0.tgz", + "integrity": "sha512-nNcjPN4SYg8drLwqK0vgVeSvxeGQiD0FxOaT38mV2H8cu0C5NzpvA+14Xy+W6vT84dxgmJYKk71Cr5QL2Oz+rA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.864.0", + "@aws-sdk/nested-clients": "3.864.0", + "@aws-sdk/types": "3.862.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/lib-storage": { + "version": "3.864.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/lib-storage/-/lib-storage-3.864.0.tgz", + "integrity": "sha512-Me/HlMXXPv3tStPQufdwnYGholY14JmmzCdOjhnG7gnaClBEnroZKcHuQhrgMm+KyfbzCQ2+9YHsULOfFrg7Mw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.0.5", + "@smithy/middleware-endpoint": "^4.1.18", + "@smithy/smithy-client": "^4.4.10", + "buffer": "5.6.0", + "events": "3.3.0", + "stream-browserify": "3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-s3": "^3.864.0" + } + }, + "node_modules/@aws-sdk/middleware-bucket-endpoint": { + "version": "3.862.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.862.0.tgz", + "integrity": "sha512-Wcsc7VPLjImQw+CP1/YkwyofMs9Ab6dVq96iS8p0zv0C6YTaMjvillkau4zFfrrrTshdzFWKptIFhKK8Zsei1g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@aws-sdk/util-arn-parser": "3.804.0", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "@smithy/util-config-provider": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-expect-continue": { + "version": "3.862.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.862.0.tgz", + "integrity": "sha512-oG3AaVUJ+26p0ESU4INFn6MmqqiBFZGrebST66Or+YBhteed2rbbFl7mCfjtPWUFgquQlvT1UP19P3LjQKeKpw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-flexible-checksums": { + "version": "3.864.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.864.0.tgz", + "integrity": "sha512-MvakvzPZi9uyP3YADuIqtk/FAcPFkyYFWVVMf5iFs/rCdk0CUzn02Qf4CSuyhbkS6Y0KrAsMgKR4MgklPU79Wg==", + "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.864.0", + "@aws-sdk/types": "3.862.0", + "@smithy/is-array-buffer": "^4.0.0", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-stream": "^4.2.4", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.862.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.862.0.tgz", + "integrity": "sha512-jDje8dCFeFHfuCAxMDXBs8hy8q9NCTlyK4ThyyfAj3U4Pixly2mmzY2u7b7AyGhWsjJNx8uhTjlYq5zkQPQCYw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-location-constraint": { + "version": "3.862.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.862.0.tgz", + "integrity": "sha512-MnwLxCw7Cc9OngEH3SHFhrLlDI9WVxaBkp3oTsdY9JE7v8OE38wQ9vtjaRsynjwu0WRtrctSHbpd7h/QVvtjyA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.862.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.862.0.tgz", + "integrity": "sha512-N/bXSJznNBR/i7Ofmf9+gM6dx/SPBK09ZWLKsW5iQjqKxAKn/2DozlnE54uiEs1saHZWoNDRg69Ww4XYYSlG1Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.862.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.862.0.tgz", + "integrity": "sha512-KVoo3IOzEkTq97YKM4uxZcYFSNnMkhW/qj22csofLegZi5fk90ztUnnaeKfaEJHfHp/tm1Y3uSoOXH45s++kKQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.864.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.864.0.tgz", + "integrity": "sha512-GjYPZ6Xnqo17NnC8NIQyvvdzzO7dm+Ks7gpxD/HsbXPmV2aEfuFveJXneGW9e1BheSKFff6FPDWu8Gaj2Iu1yg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.864.0", + "@aws-sdk/types": "3.862.0", + "@aws-sdk/util-arn-parser": "3.804.0", + "@smithy/core": "^3.8.0", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/protocol-http": "^5.1.3", + "@smithy/signature-v4": "^5.1.3", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-stream": "^4.2.4", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-ssec": { + "version": "3.862.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.862.0.tgz", + "integrity": "sha512-72VtP7DZC8lYTE2L3Efx2BrD98oe9WTK8X6hmd3WTLkbIjvgWQWIdjgaFXBs8WevsXkewIctfyA3KEezvL5ggw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.864.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.864.0.tgz", + "integrity": "sha512-wrddonw4EyLNSNBrApzEhpSrDwJiNfjxDm5E+bn8n32BbAojXASH8W8jNpxz/jMgNkkJNxCfyqybGKzBX0OhbQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.864.0", + "@aws-sdk/types": "3.862.0", + "@aws-sdk/util-endpoints": "3.862.0", + "@smithy/core": "^3.8.0", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.864.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.864.0.tgz", + "integrity": "sha512-H1C+NjSmz2y8Tbgh7Yy89J20yD/hVyk15hNoZDbCYkXg0M358KS7KVIEYs8E2aPOCr1sK3HBE819D/yvdMgokA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.864.0", + "@aws-sdk/middleware-host-header": "3.862.0", + "@aws-sdk/middleware-logger": "3.862.0", + "@aws-sdk/middleware-recursion-detection": "3.862.0", + "@aws-sdk/middleware-user-agent": "3.864.0", + "@aws-sdk/region-config-resolver": "3.862.0", + "@aws-sdk/types": "3.862.0", + "@aws-sdk/util-endpoints": "3.862.0", + "@aws-sdk/util-user-agent-browser": "3.862.0", + "@aws-sdk/util-user-agent-node": "3.864.0", + "@smithy/config-resolver": "^4.1.5", + "@smithy/core": "^3.8.0", + "@smithy/fetch-http-handler": "^5.1.1", + "@smithy/hash-node": "^4.0.5", + "@smithy/invalid-dependency": "^4.0.5", + "@smithy/middleware-content-length": "^4.0.5", + "@smithy/middleware-endpoint": "^4.1.18", + "@smithy/middleware-retry": "^4.1.19", + "@smithy/middleware-serde": "^4.0.9", + "@smithy/middleware-stack": "^4.0.5", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/node-http-handler": "^4.1.1", + "@smithy/protocol-http": "^5.1.3", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.26", + "@smithy/util-defaults-mode-node": "^4.0.26", + "@smithy/util-endpoints": "^3.0.7", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-retry": "^4.0.7", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.862.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.862.0.tgz", + "integrity": "sha512-VisR+/HuVFICrBPY+q9novEiE4b3mvDofWqyvmxHcWM7HumTz9ZQSuEtnlB/92GVM3KDUrR9EmBHNRrfXYZkcQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/types": "^4.3.2", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/s3-request-presigner": { + "version": "3.864.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.864.0.tgz", + "integrity": "sha512-IiVFDxabrqTB1A9qZI6IEa3cOgF2eciUG4UX27HzkMY6UXG0EZhnGkgkgHYMt6j2hGAFOvAh0ogv/XxZLg6Zaw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/signature-v4-multi-region": "3.864.0", + "@aws-sdk/types": "3.862.0", + "@aws-sdk/util-format-url": "3.862.0", + "@smithy/middleware-endpoint": "^4.1.18", + "@smithy/protocol-http": "^5.1.3", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.864.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.864.0.tgz", + "integrity": "sha512-w2HIn/WIcUyv1bmyCpRUKHXB5KdFGzyxPkp/YK5g+/FuGdnFFYWGfcO8O+How4jwrZTarBYsAHW9ggoKvwr37w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-sdk-s3": "3.864.0", + "@aws-sdk/types": "3.862.0", + "@smithy/protocol-http": "^5.1.3", + "@smithy/signature-v4": "^5.1.3", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.864.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.864.0.tgz", + "integrity": "sha512-gTc2QHOBo05SCwVA65dUtnJC6QERvFaPiuppGDSxoF7O5AQNK0UR/kMSenwLqN8b5E1oLYvQTv3C1idJLRX0cg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.864.0", + "@aws-sdk/nested-clients": "3.864.0", + "@aws-sdk/types": "3.862.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.862.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.862.0.tgz", + "integrity": "sha512-Bei+RL0cDxxV+lW2UezLbCYYNeJm6Nzee0TpW0FfyTRBhH9C1XQh4+x+IClriXvgBnRquTMMYsmJfvx8iyLKrg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-arn-parser": { + "version": "3.804.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.804.0.tgz", + "integrity": "sha512-wmBJqn1DRXnZu3b4EkE6CWnoWMo1ZMvlfkqU5zPz67xx1GMaXlDCchFvKAXMjk4jn/L1O3tKnoFDNsoLV1kgNQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.862.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.862.0.tgz", + "integrity": "sha512-eCZuScdE9MWWkHGM2BJxm726MCmWk/dlHjOKvkM0sN1zxBellBMw5JohNss1Z8/TUmnW2gb9XHTOiHuGjOdksA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", + "@smithy/util-endpoints": "^3.0.7", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-format-url": { + "version": "3.862.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.862.0.tgz", + "integrity": "sha512-4kd2PYUMA/fAnIcVVwBIDCa2KCuUPrS3ELgScLjBaESP0NN+K163m40U5RbzNec/elOcJHR8lEThzzSb7vXH6w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@smithy/querystring-builder": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.804.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.804.0.tgz", + "integrity": "sha512-zVoRfpmBVPodYlnMjgVjfGoEZagyRF5IPn3Uo6ZvOZp24chnW/FRstH7ESDHDDRga4z3V+ElUQHKpFDXWyBW5A==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.862.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.862.0.tgz", + "integrity": "sha512-BmPTlm0r9/10MMr5ND9E92r8KMZbq5ltYXYpVcUbAsnB1RJ8ASJuRoLne5F7mB3YMx0FJoOTuSq7LdQM3LgW3Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@smithy/types": "^4.3.2", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.864.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.864.0.tgz", + "integrity": "sha512-d+FjUm2eJEpP+FRpVR3z6KzMdx1qwxEYDz8jzNKwxYLBBquaBaP/wfoMtMQKAcbrR7aT9FZVZF7zDgzNxUvQlQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.864.0", + "@aws-sdk/types": "3.862.0", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.862.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.862.0.tgz", + "integrity": "sha512-6Ed0kmC1NMbuFTEgNmamAUU1h5gShgxL1hBVLbEzUa3trX5aJBz1vU4bXaBTvOYUAnOHtiy1Ml4AMStd6hJnFA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/abort-controller": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.5.tgz", + "integrity": "sha512-jcrqdTQurIrBbUm4W2YdLVMQDoL0sA9DTxYd2s+R/y+2U9NLOP7Xf/YqfSg1FZhlZIYEnvk2mwbyvIfdLEPo8g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/chunked-blob-reader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-5.0.0.tgz", + "integrity": "sha512-+sKqDBQqb036hh4NPaUiEkYFkTUGYzRsn3EuFhyfQfMy6oGHEUJDurLP9Ufb5dasr/XiAmPNMr6wa9afjQB+Gw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/chunked-blob-reader-native": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-4.0.0.tgz", + "integrity": "sha512-R9wM2yPmfEMsUmlMlIgSzOyICs0x9uu7UTHoccMyt7BWw8shcGM8HqB355+BZCPBcySvbTYMs62EgEQkNxz2ig==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-base64": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.1.5.tgz", + "integrity": "sha512-viuHMxBAqydkB0AfWwHIdwf/PRH2z5KHGUzqyRtS/Wv+n3IHI993Sk76VCA7dD/+GzgGOmlJDITfPcJC1nIVIw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.1.4", + "@smithy/types": "^4.3.2", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.8.0.tgz", + "integrity": "sha512-EYqsIYJmkR1VhVE9pccnk353xhs+lB6btdutJEtsp7R055haMJp2yE16eSxw8fv+G0WUY6vqxyYOP8kOqawxYQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-serde": "^4.0.9", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-stream": "^4.2.4", + "@smithy/util-utf8": "^4.0.0", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.0.7.tgz", + "integrity": "sha512-dDzrMXA8d8riFNiPvytxn0mNwR4B3h8lgrQ5UjAGu6T9z/kRg/Xncf4tEQHE/+t25sY8IH3CowcmWi+1U5B1Gw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.1.4", + "@smithy/property-provider": "^4.0.5", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-codec": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.0.5.tgz", + "integrity": "sha512-miEUN+nz2UTNoRYRhRqVTJCx7jMeILdAurStT2XoS+mhokkmz1xAPp95DFW9Gxt4iF2VBqpeF9HbTQ3kY1viOA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.3.2", + "@smithy/util-hex-encoding": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-browser": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.0.5.tgz", + "integrity": "sha512-LCUQUVTbM6HFKzImYlSB9w4xafZmpdmZsOh9rIl7riPC3osCgGFVP+wwvYVw6pXda9PPT9TcEZxaq3XE81EdJQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-config-resolver": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.1.3.tgz", + "integrity": "sha512-yTTzw2jZjn/MbHu1pURbHdpjGbCuMHWncNBpJnQAPxOVnFUAbSIUSwafiphVDjNV93TdBJWmeVAds7yl5QCkcA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-node": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.0.5.tgz", + "integrity": "sha512-lGS10urI4CNzz6YlTe5EYG0YOpsSp3ra8MXyco4aqSkQDuyZPIw2hcaxDU82OUVtK7UY9hrSvgWtpsW5D4rb4g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-universal": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.0.5.tgz", + "integrity": "sha512-JFnmu4SU36YYw3DIBVao3FsJh4Uw65vVDIqlWT4LzR6gXA0F3KP0IXFKKJrhaVzCBhAuMsrUUaT5I+/4ZhF7aw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-codec": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.1.1.tgz", + "integrity": "sha512-61WjM0PWmZJR+SnmzaKI7t7G0UkkNFboDpzIdzSoy7TByUzlxo18Qlh9s71qug4AY4hlH/CwXdubMtkcNEb/sQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.1.3", + "@smithy/querystring-builder": "^4.0.5", + "@smithy/types": "^4.3.2", + "@smithy/util-base64": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-blob-browser": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.0.5.tgz", + "integrity": "sha512-F7MmCd3FH/Q2edhcKd+qulWkwfChHbc9nhguBlVjSUE6hVHhec3q6uPQ+0u69S6ppvLtR3eStfCuEKMXBXhvvA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/chunked-blob-reader": "^5.0.0", + "@smithy/chunked-blob-reader-native": "^4.0.0", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.0.5.tgz", + "integrity": "sha512-cv1HHkKhpyRb6ahD8Vcfb2Hgz67vNIXEp2vnhzfxLFGRukLCNEA5QdsorbUEzXma1Rco0u3rx5VTqbM06GcZqQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-stream-node": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.0.5.tgz", + "integrity": "sha512-IJuDS3+VfWB67UC0GU0uYBG/TA30w+PlOaSo0GPm9UHS88A6rCP6uZxNjNYiyRtOcjv7TXn/60cW8ox1yuZsLg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.0.5.tgz", + "integrity": "sha512-IVnb78Qtf7EJpoEVo7qJ8BEXQwgC4n3igeJNNKEj/MLYtapnx8A67Zt/J3RXAj2xSO1910zk0LdFiygSemuLow==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.0.0.tgz", + "integrity": "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/md5-js": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.0.5.tgz", + "integrity": "sha512-8n2XCwdUbGr8W/XhMTaxILkVlw2QebkVTn5tm3HOcbPbOpWg89zr6dPXsH8xbeTsbTXlJvlJNTQsKAIoqQGbdA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.0.5.tgz", + "integrity": "sha512-l1jlNZoYzoCC7p0zCtBDE5OBXZ95yMKlRlftooE5jPWQn4YBPLgsp+oeHp7iMHaTGoUdFqmHOPa8c9G3gBsRpQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.1.18.tgz", + "integrity": "sha512-ZhvqcVRPZxnZlokcPaTwb+r+h4yOIOCJmx0v2d1bpVlmP465g3qpVSf7wxcq5zZdu4jb0H4yIMxuPwDJSQc3MQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.8.0", + "@smithy/middleware-serde": "^4.0.9", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", + "@smithy/util-middleware": "^4.0.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "4.1.19", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.1.19.tgz", + "integrity": "sha512-X58zx/NVECjeuUB6A8HBu4bhx72EoUz+T5jTMIyeNKx2lf+Gs9TmWPNNkH+5QF0COjpInP/xSpJGJ7xEnAklQQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.1.4", + "@smithy/protocol-http": "^5.1.3", + "@smithy/service-error-classification": "^4.0.7", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-retry": "^4.0.7", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.0.9.tgz", + "integrity": "sha512-uAFFR4dpeoJPGz8x9mhxp+RPjo5wW0QEEIPPPbLXiRRWeCATf/Km3gKIVR5vaP8bN1kgsPhcEeh+IZvUlBv6Xg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.0.5.tgz", + "integrity": "sha512-/yoHDXZPh3ocRVyeWQFvC44u8seu3eYzZRveCMfgMOBcNKnAmOvjbL9+Cp5XKSIi9iYA9PECUuW2teDAk8T+OQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.1.4.tgz", + "integrity": "sha512-+UDQV/k42jLEPPHSn39l0Bmc4sB1xtdI9Gd47fzo/0PbXzJ7ylgaOByVjF5EeQIumkepnrJyfx86dPa9p47Y+w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.1.1.tgz", + "integrity": "sha512-RHnlHqFpoVdjSPPiYy/t40Zovf3BBHc2oemgD7VsVTFFZrU5erFFe0n52OANZZ/5sbshgD93sOh5r6I35Xmpaw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.0.5", + "@smithy/protocol-http": "^5.1.3", + "@smithy/querystring-builder": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.0.5.tgz", + "integrity": "sha512-R/bswf59T/n9ZgfgUICAZoWYKBHcsVDurAGX88zsiUtOTA/xUAPyiT+qkNCPwFn43pZqN84M4MiUsbSGQmgFIQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.3.tgz", + "integrity": "sha512-fCJd2ZR7D22XhDY0l+92pUag/7je2BztPRQ01gU5bMChcyI0rlly7QFibnYHzcxDvccMjlpM/Q1ev8ceRIb48w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.0.5.tgz", + "integrity": "sha512-NJeSCU57piZ56c+/wY+AbAw6rxCCAOZLCIniRE7wqvndqxcKKDOXzwWjrY7wGKEISfhL9gBbAaWWgHsUGedk+A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "@smithy/util-uri-escape": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.0.5.tgz", + "integrity": "sha512-6SV7md2CzNG/WUeTjVe6Dj8noH32r4MnUeFKZrnVYsQxpGSIcphAanQMayi8jJLZAWm6pdM9ZXvKCpWOsIGg0w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.0.7.tgz", + "integrity": "sha512-XvRHOipqpwNhEjDf2L5gJowZEm5nsxC16pAZOeEcsygdjv9A2jdOh3YoDQvOXBGTsaJk6mNWtzWalOB9976Wlg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.0.5.tgz", + "integrity": "sha512-YVVwehRDuehgoXdEL4r1tAAzdaDgaC9EQvhK0lEbfnbrd0bd5+CTQumbdPryX3J2shT7ZqQE+jPW4lmNBAB8JQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.1.3.tgz", + "integrity": "sha512-mARDSXSEgllNzMw6N+mC+r1AQlEBO3meEAkR/UlfAgnMzJUB3goRBWgip1EAMG99wh36MDqzo86SfIX5Y+VEaw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.0.0", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-uri-escape": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "4.4.10", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.4.10.tgz", + "integrity": "sha512-iW6HjXqN0oPtRS0NK/zzZ4zZeGESIFcxj2FkWed3mcK8jdSdHzvnCKXSjvewESKAgGKAbJRA+OsaqKhkdYRbQQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.8.0", + "@smithy/middleware-endpoint": "^4.1.18", + "@smithy/middleware-stack": "^4.0.5", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "@smithy/util-stream": "^4.2.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.3.2.tgz", + "integrity": "sha512-QO4zghLxiQ5W9UZmX2Lo0nta2PuE1sSrXUYDoaB6HMR762C0P7v/HEPHf6ZdglTVssJG1bsrSBxdc3quvDSihw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.0.5.tgz", + "integrity": "sha512-j+733Um7f1/DXjYhCbvNXABV53NyCRRA54C7bNEIxNPs0YjfRxeMKjjgm2jvTYrciZyCjsicHwQ6Q0ylo+NAUw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-base64": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.0.0.tgz", + "integrity": "sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.0.0.tgz", + "integrity": "sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.0.0.tgz", + "integrity": "sha512-q0iDP3VsZzqJyje8xJWEJCNIu3lktUGVoSy1KB0UWym2CL1siV3artm+u1DFYTLejpsrdGyCSWBdGNjJzfDPjg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.0.0.tgz", + "integrity": "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.0.0.tgz", + "integrity": "sha512-L1RBVzLyfE8OXH+1hsJ8p+acNUSirQnWQ6/EgpchV88G6zGBTDPdXiiExei6Z1wR2RxYvxY/XLw6AMNCCt8H3w==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.0.26", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.0.26.tgz", + "integrity": "sha512-xgl75aHIS/3rrGp7iTxQAOELYeyiwBu+eEgAk4xfKwJJ0L8VUjhO2shsDpeil54BOFsqmk5xfdesiewbUY5tKQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.0.5", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.0.26", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.0.26.tgz", + "integrity": "sha512-z81yyIkGiLLYVDetKTUeCZQ8x20EEzvQjrqJtb/mXnevLq2+w3XCEWTJ2pMp401b6BkEkHVfXb/cROBpVauLMQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^4.1.5", + "@smithy/credential-provider-imds": "^4.0.7", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/property-provider": "^4.0.5", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.0.7.tgz", + "integrity": "sha512-klGBP+RpBp6V5JbrY2C/VKnHXn3d5V2YrifZbmMY8os7M6m8wdYFoO6w/fe5VkP+YVwrEktW3IWYaSQVNZJ8oQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.1.4", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.0.0.tgz", + "integrity": "sha512-Yk5mLhHtfIgW2W2WQZWSg5kuMZCVbvhFmC7rV4IO2QqnZdbEFPmQnCcGMAX2z/8Qj3B9hYYNjZOhWym+RwhePw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.0.5.tgz", + "integrity": "sha512-N40PfqsZHRSsByGB81HhSo+uvMxEHT+9e255S53pfBw/wI6WKDI7Jw9oyu5tJTLwZzV5DsMha3ji8jk9dsHmQQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-retry": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.0.7.tgz", + "integrity": "sha512-TTO6rt0ppK70alZpkjwy+3nQlTiqNfoXja+qwuAchIEAIoSZW8Qyd76dvBv3I5bCpE38APafG23Y/u270NspiQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^4.0.7", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-stream": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.2.4.tgz", + "integrity": "sha512-vSKnvNZX2BXzl0U2RgCLOwWaAP9x/ddd/XobPK02pCbzRm5s55M53uwb1rl/Ts7RXZvdJZerPkA+en2FDghLuQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/fetch-http-handler": "^5.1.1", + "@smithy/node-http-handler": "^4.1.1", + "@smithy/types": "^4.3.2", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-uri-escape": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.0.0.tgz", + "integrity": "sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.0.0.tgz", + "integrity": "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-waiter": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.0.7.tgz", + "integrity": "sha512-mYqtQXPmrwvUljaHyGxYUIIRI3qjBTEb/f5QFi3A6VlxhpmZd5mWXn9W+qUkf2pVE1Hv3SqxefiZOPGdxmO64A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -50,6 +1728,12 @@ "undici-types": "~7.8.0" } }, + "node_modules/@types/uuid": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", + "license": "MIT" + }, "node_modules/@types/validator": { "version": "13.15.2", "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.2.tgz", @@ -83,6 +1767,12 @@ "node": ">= 8" } }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -90,6 +1780,26 @@ "dev": true, "license": "MIT" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/basic-auth": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", @@ -155,6 +1865,12 @@ "node": ">=18" } }, + "node_modules/bowser": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.12.0.tgz", + "integrity": "sha512-HcOcTudTeEWgbHh0Y1Tyb6fdeR71m4b/QACf0D4KswGTsNeIJQmg38mRENZPAYPZvGFN3fk3604XbQEPdxXdKg==", + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -179,12 +1895,39 @@ "node": ">=8" } }, + "node_modules/buffer": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz", + "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4" + } + }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", "license": "BSD-3-Clause" }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -255,6 +1998,21 @@ "dev": true, "license": "MIT" }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, "node_modules/content-disposition": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", @@ -453,6 +2211,15 @@ "node": ">= 0.6" } }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/express": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", @@ -495,6 +2262,24 @@ "url": "https://opencollective.com/express" } }, + "node_modules/fast-xml-parser": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", + "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^2.1.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -691,6 +2476,26 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore-by-default": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", @@ -929,6 +2734,27 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/moment": { "version": "2.30.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", @@ -999,6 +2825,67 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/multer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", + "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "mkdirp": "^0.5.6", + "object-assign": "^4.1.1", + "type-is": "^1.6.18", + "xtend": "^4.0.2" + }, + "engines": { + "node": ">= 10.16.0" + } + }, + "node_modules/multer/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/negotiator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", @@ -1336,6 +3223,20 @@ "node": ">= 0.8" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -1626,6 +3527,45 @@ "node": ">= 0.8" } }, + "node_modules/stream-browserify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", + "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==", + "license": "MIT", + "dependencies": { + "inherits": "~2.0.4", + "readable-stream": "^3.5.0" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strnum": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", + "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -1677,6 +3617,12 @@ "nodetouch": "bin/nodetouch.js" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/type-is": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", @@ -1691,6 +3637,12 @@ "node": ">= 0.6" } }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -1713,6 +3665,12 @@ "node": ">= 0.8" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", diff --git a/package.json b/package.json index 7e0a0af..ed8225a 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,9 @@ "license": "ISC", "description": "", "dependencies": { + "@aws-sdk/client-s3": "^3.864.0", + "@aws-sdk/lib-storage": "^3.864.0", + "@aws-sdk/s3-request-presigner": "^3.864.0", "bcrypt": "^6.0.0", "cookie-parser": "^1.4.7", "cors": "^2.8.5", @@ -20,6 +23,7 @@ "express": "^5.1.0", "jsonwebtoken": "^9.0.2", "morgan": "^1.10.0", + "multer": "^2.0.2", "pg": "^8.16.2", "sequelize": "^6.37.7" },