From c1ea56a7c1a27af3dd877ee6cf07d46cdcdaecbc Mon Sep 17 00:00:00 2001 From: Rajat Saxena Date: Tue, 28 Oct 2025 23:51:57 +0530 Subject: [PATCH 1/8] WIP: tus routes; upload creation done; upload offset is getting updated in the db --- apps/api/package.json | 2 + apps/api/src/apikey/middleware.ts | 1 + apps/api/src/config/constants.ts | 8 + apps/api/src/index.ts | 12 + apps/api/src/presigning/handlers.ts | 27 ++ apps/api/src/presigning/routes.ts | 3 +- apps/api/src/presigning/service.ts | 23 ++ apps/api/src/tus/finalize.ts | 203 ++++++++++++ apps/api/src/tus/model.ts | 43 +++ apps/api/src/tus/queries.ts | 87 +++++ apps/api/src/tus/routes.ts | 147 +++++++++ apps/api/src/tus/tus-server.ts | 227 +++++++++++++ examples/next-app-router/README.md | 24 ++ .../app/api/medialit/tus/route.ts | 33 ++ examples/next-app-router/app/page.tsx | 41 ++- .../components/TusUploadForm.tsx | 264 +++++++++++++++ examples/next-app-router/package.json | 3 +- pnpm-lock.yaml | 310 +++++++++++++++++- 18 files changed, 1436 insertions(+), 22 deletions(-) create mode 100644 apps/api/src/tus/finalize.ts create mode 100644 apps/api/src/tus/model.ts create mode 100644 apps/api/src/tus/queries.ts create mode 100644 apps/api/src/tus/routes.ts create mode 100644 apps/api/src/tus/tus-server.ts create mode 100644 examples/next-app-router/app/api/medialit/tus/route.ts create mode 100644 examples/next-app-router/components/TusUploadForm.tsx diff --git a/apps/api/package.json b/apps/api/package.json index cbc2e8ad..172c1886 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -38,6 +38,8 @@ "@medialit/models": "workspace:*", "@medialit/thumbnail": "workspace:*", "@medialit/utils": "workspace:^0.1.0", + "@tus/file-store": "^2.0.0", + "@tus/server": "^2.3.0", "aws-sdk": "^2.1692.0", "cors": "^2.8.5", "dotenv": "^16.4.7", diff --git a/apps/api/src/apikey/middleware.ts b/apps/api/src/apikey/middleware.ts index 98b58dec..4fe8e2b7 100644 --- a/apps/api/src/apikey/middleware.ts +++ b/apps/api/src/apikey/middleware.ts @@ -15,6 +15,7 @@ export default async function apikey( const reqKey = req.body.apikey; if (!reqKey) { + console.log("API key missing in request"); return res.status(400).json({ error: BAD_REQUEST }); } diff --git a/apps/api/src/config/constants.ts b/apps/api/src/config/constants.ts index fc2a6919..24829137 100644 --- a/apps/api/src/config/constants.ts +++ b/apps/api/src/config/constants.ts @@ -62,3 +62,11 @@ export const CDN_MAX_AGE = process.env.CDN_MAX_AGE export const ENDPOINT = USE_CLOUDFRONT ? CLOUDFRONT_ENDPOINT : S3_ENDPOINT; export const HOSTNAME_OVERRIDE = process.env.HOSTNAME_OVERRIDE || ""; // Useful for hosting via Docker + +// Tus upload config +export const TUS_UPLOAD_EXPIRATION_HOURS = parseInt( + process.env.TUS_UPLOAD_EXPIRATION_HOURS || "48", +); +export const TUS_CHUNK_SIZE = parseInt( + process.env.TUS_CHUNK_SIZE || "10485760", +); // 10MB default diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index 7fe885ab..e885f6be 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -7,11 +7,13 @@ import passport from "passport"; import mediaRoutes from "./media/routes"; import presignedUrlRoutes from "./presigning/routes"; import mediaSettingsRoutes from "./media-settings/routes"; +import tusRoutes from "./tus/routes"; import logger from "./services/log"; import { createUser, findByEmail } from "./user/queries"; import { Apikey, Constants, User } from "@medialit/models"; import { createApiKey } from "./apikey/queries"; import { spawn } from "child_process"; +import { cleanupExpiredTusUploads } from "./tus/queries"; connectToDatabase(); const app = express(); @@ -29,7 +31,9 @@ app.get("/health", (req, res) => { app.use("/settings/media", mediaSettingsRoutes(passport)); app.use("/media/presigned", presignedUrlRoutes); +app.use("/media", tusRoutes); app.use("/media", mediaRoutes); +// Mount TUS routes under /media so paths like /media/create/tus match correctly const port = process.env.PORT || 80; @@ -41,6 +45,14 @@ checkDependencies().then(() => { app.listen(port, () => { logger.info(`Medialit server running at ${port}`); }); + + // Setup background cleanup job for expired tus uploads + setInterval( + async () => { + await cleanupExpiredTusUploads(); + }, + 1000 * 60 * 60, + ); // Run every hour }); async function checkDependencies() { diff --git a/apps/api/src/presigning/handlers.ts b/apps/api/src/presigning/handlers.ts index 6d3084a3..b2d31147 100644 --- a/apps/api/src/presigning/handlers.ts +++ b/apps/api/src/presigning/handlers.ts @@ -36,3 +36,30 @@ export async function getPresignedUrl( return res.status(500).json(err.message); } } + +export async function getPresignedTusUrl( + req: any, + res: any, + next: (...args: any[]) => void, +) { + const { error } = validatePresigningOptions(req); + if (error) { + return res.status(400).json({ error: error.message }); + } + + try { + const presignedUrl = await preSignedUrlService.generateSignedUrlForTus({ + userId: req.user.id, + apikey: req.apikey, + protocol: req.protocol, + host: HOSTNAME_OVERRIDE || req.get("Host"), + group: req.body.group, + }); + // Extract signature from the URL + const signature = presignedUrl.split("signature=")[1]; + return res.status(200).json({ signature }); + } catch (err: any) { + logger.error({ err }, err.message); + return res.status(500).json(err.message); + } +} diff --git a/apps/api/src/presigning/routes.ts b/apps/api/src/presigning/routes.ts index d1ff698b..47ef8afd 100644 --- a/apps/api/src/presigning/routes.ts +++ b/apps/api/src/presigning/routes.ts @@ -1,8 +1,9 @@ import express from "express"; import apikey from "../apikey/middleware"; -import { getPresignedUrl } from "./handlers"; +import { getPresignedUrl, getPresignedTusUrl } from "./handlers"; const router = express.Router(); router.post("/create", apikey, getPresignedUrl); +router.post("/tus/create", apikey, getPresignedTusUrl); export default router; diff --git a/apps/api/src/presigning/service.ts b/apps/api/src/presigning/service.ts index 781ac2f7..4577e353 100644 --- a/apps/api/src/presigning/service.ts +++ b/apps/api/src/presigning/service.ts @@ -69,3 +69,26 @@ export async function cleanup(userId: string, signature: string) { await queries.deleteBySignature(signature); await queries.cleanupExpiredLinks(userId); } + +export async function generateSignedUrlForTus({ + userId, + apikey, + protocol, + host, + group, +}: GenerateSignedUrlProps): Promise { + const presignedUrl = await queries.createPresignedUrl( + userId, + apikey, + group, + ); + + queries.cleanupExpiredLinks(userId).catch((err: any) => { + logger.error( + { err }, + `Error while cleaning up expired links for ${userId}`, + ); + }); + + return `${protocol}://${host}/media/create/tus?signature=${presignedUrl?.signature}`; +} diff --git a/apps/api/src/tus/finalize.ts b/apps/api/src/tus/finalize.ts new file mode 100644 index 00000000..d72aff77 --- /dev/null +++ b/apps/api/src/tus/finalize.ts @@ -0,0 +1,203 @@ +import { readFileSync, createReadStream, rmdirSync, existsSync } from "fs"; +import path from "path"; +import thumbnail from "@medialit/thumbnail"; +import mongoose from "mongoose"; +import { + tempFileDirForUploads, + imagePattern, + imagePatternForThumbnailGeneration, + videoPattern, + USE_CLOUDFRONT, +} from "../config/constants"; +import imageUtils from "@medialit/images"; +import { + foldersExist, + createFolders, +} from "../media/utils/manage-files-on-disk"; +import type { MediaWithUserId } from "../media/model"; +import { putObject, UploadParams } from "../services/s3"; +import logger from "../services/log"; +import generateKey from "../media/utils/generate-key"; +import { getMediaSettings } from "../media-settings/queries"; +import generateFileName from "../media/utils/generate-file-name"; +import { createMedia } from "../media/queries"; +import getTags from "../media/utils/get-tags"; +import { getTusUpload, markTusUploadComplete } from "./queries"; +import * as presignedUrlService from "../presigning/service"; + +const generateAndUploadThumbnail = async ({ + workingDirectory, + key, + mimetype, + originalFilePath, + tags, +}: { + workingDirectory: string; + key: string; + mimetype: string; + originalFilePath: string; + tags: string; +}): Promise => { + const thumbPath = `${workingDirectory}/thumb.webp`; + + let isThumbGenerated = false; + if (imagePatternForThumbnailGeneration.test(mimetype)) { + await thumbnail.forImage(originalFilePath, thumbPath); + isThumbGenerated = true; + } + if (videoPattern.test(mimetype)) { + await thumbnail.forVideo(originalFilePath, thumbPath); + isThumbGenerated = true; + } + + if (isThumbGenerated) { + await putObject({ + Key: key, + Body: createReadStream(thumbPath), + ContentType: "image/webp", + ACL: USE_CLOUDFRONT ? "private" : "public-read", + Tagging: tags, + }); + } + + return isThumbGenerated; +}; + +export default async function finalizeUpload(uploadId: string) { + logger.info({ uploadId }, "Finalizing tus upload"); + + const tusUpload = await getTusUpload(uploadId); + if (!tusUpload) { + throw new Error(`Tus upload not found: ${uploadId}`); + } + + if (tusUpload.isComplete) { + logger.info({ uploadId }, "Upload already finalized"); + return; + } + + const { userId, apikey, metadata, uploadLength, tempFilePath, signature } = + tusUpload; + + // Read the completed file from tus data store + const tusFilePath = path.join( + `${tempFileDirForUploads}/tus-uploads`, + tempFilePath, + ); + + if (!existsSync(tusFilePath)) { + logger.error({ uploadId, tusFilePath }, "Tus file not found"); + throw new Error(`Tus file not found: ${tusFilePath}`); + } + + const mediaSettings = await getMediaSettings(userId, apikey); + const useWebP = mediaSettings?.useWebP || false; + const webpOutputQuality = mediaSettings?.webpOutputQuality || 0; + + // Generate unique media ID + const fileName = generateFileName(metadata.filename); + const temporaryFolderForWork = `${tempFileDirForUploads}/${fileName.name}`; + if (!foldersExist([temporaryFolderForWork])) { + createFolders([temporaryFolderForWork]); + } + + let fileExtension = path.extname(metadata.filename).replace(".", ""); + let mimeType = metadata.mimetype; + if (useWebP && imagePattern.test(mimeType)) { + fileExtension = "webp"; + mimeType = "image/webp"; + } + + const mainFilePath = `${temporaryFolderForWork}/main.${fileExtension}`; + + // Copy file from tus store to working directory + const tusFileContent = readFileSync(tusFilePath); + require("fs").writeFileSync(mainFilePath, tusFileContent); + + // Apply WebP conversion if needed + if (useWebP && imagePattern.test(metadata.mimetype)) { + await imageUtils.convertToWebp(mainFilePath, webpOutputQuality); + } + + const uploadParams: UploadParams = { + Key: generateKey({ + mediaId: fileName.name, + access: metadata.access === "public" ? "public" : "private", + filename: `main.${fileExtension}`, + }), + Body: createReadStream(mainFilePath), + ContentType: mimeType, + ACL: USE_CLOUDFRONT + ? "private" + : metadata.access === "public" + ? "public-read" + : "private", + }; + const tags = getTags(userId, metadata.group); + uploadParams.Tagging = tags; + + await putObject(uploadParams); + + let isThumbGenerated = false; + try { + isThumbGenerated = await generateAndUploadThumbnail({ + workingDirectory: temporaryFolderForWork, + mimetype: metadata.mimetype, + originalFilePath: mainFilePath, + key: generateKey({ + mediaId: fileName.name, + access: "public", + filename: "thumb.webp", + }), + tags, + }); + } catch (err: any) { + logger.error({ err }, err.message); + } + + rmdirSync(temporaryFolderForWork, { recursive: true }); + + const mediaObject: MediaWithUserId = { + fileName: `main.${fileExtension}`, + mediaId: fileName.name, + userId: new mongoose.Types.ObjectId(userId), + apikey, + originalFileName: metadata.filename, + mimeType, + size: uploadLength, + thumbnailGenerated: isThumbGenerated, + caption: metadata.caption, + accessControl: metadata.access === "public" ? "public-read" : "private", + group: metadata.group, + }; + const media = await createMedia(mediaObject); + + // Mark upload as complete + await markTusUploadComplete(uploadId); + + // Cleanup presigned URL if used + if (signature) { + presignedUrlService.cleanup(userId, signature).catch((err: any) => { + logger.error( + { err }, + `Error in cleaning up expired links for ${userId}`, + ); + }); + } + + // Cleanup tus file + try { + if (existsSync(tusFilePath)) { + require("fs").unlinkSync(tusFilePath); + } + } catch (err) { + logger.error({ err }, "Error cleaning up tus file"); + } + + logger.info( + { uploadId, mediaId: media.mediaId }, + "Tus upload finalized successfully", + ); + + return media.mediaId; +} diff --git a/apps/api/src/tus/model.ts b/apps/api/src/tus/model.ts new file mode 100644 index 00000000..44a1d1ef --- /dev/null +++ b/apps/api/src/tus/model.ts @@ -0,0 +1,43 @@ +import type { Media } from "@medialit/models"; +import mongoose from "mongoose"; + +export interface TusUpload { + uploadId: string; + userId: string; + apikey: string; + uploadLength: number; + uploadOffset: number; + metadata: Pick< + Media, + "fileName" | "mimeType" | "accessControl" | "caption" | "group" + >; + tempFilePath: string; + isComplete: boolean; + expiresAt?: Date; + signature?: string; +} + +const TusUploadSchema = new mongoose.Schema( + { + uploadId: { type: String, required: true, unique: true }, + userId: { type: String, required: true }, + apikey: { type: String, required: true }, + uploadLength: { type: Number, required: true }, + uploadOffset: { type: Number, required: true, default: 0 }, + metadata: { + fileName: { type: String, required: true }, + mimeType: { type: String, required: true }, + accessControl: { type: String, required: true, default: "private" }, + caption: String, + group: String, + }, + signature: String, + tempFilePath: String, + isComplete: Boolean, + expiresAt: Date, + }, + { timestamps: true }, +); + +export default mongoose.models.TusUpload || + mongoose.model("TusUpload", TusUploadSchema); diff --git a/apps/api/src/tus/queries.ts b/apps/api/src/tus/queries.ts new file mode 100644 index 00000000..bf68a6f2 --- /dev/null +++ b/apps/api/src/tus/queries.ts @@ -0,0 +1,87 @@ +import logger from "../services/log"; +import TusUploadModel, { TusUpload } from "./model"; + +type TusUploadDocument = any; + +export async function createTusUpload( + data: Omit, +): Promise { + // const uploadId = new mongoose.Types.ObjectId().toString(); + const expiresAt = new Date(); + expiresAt.setHours( + expiresAt.getHours() + + parseInt(process.env.TUS_UPLOAD_EXPIRATION_HOURS || "48"), + ); + + const tusUploadData: TusUpload = { + uploadId: data.uploadId, + userId: data.userId, + apikey: data.apikey, + uploadLength: data.uploadLength, + metadata: data.metadata, + tempFilePath: data.tempFilePath, + signature: data.signature, + uploadOffset: 0, + isComplete: false, + expiresAt, + }; + console.log( + "Creating TusUpload with ID:", + data.uploadId, + data, + tusUploadData, + ); + const tusUpload = await TusUploadModel.create(tusUploadData); + + return tusUpload; +} + +export async function getTusUpload( + uploadId: string, +): Promise { + return TusUploadModel.findOne({ uploadId }); +} + +// export async function getTusUploadBySignature( +// signature: string, +// ): Promise { +// const TusUploadModel = getTusUploadModel(); +// return TusUploadModel.findOne({ signature }); +// } + +export async function updateTusUploadOffset( + uploadId: string, + uploadOffset: number, +): Promise { + await TusUploadModel.updateOne({ uploadId }, { uploadOffset }); +} + +export async function markTusUploadComplete(uploadId: string): Promise { + await TusUploadModel.updateOne({ uploadId }, { isComplete: true }); +} + +export async function deleteTusUpload(uploadId: string): Promise { + await TusUploadModel.deleteOne({ uploadId }); +} + +export async function cleanupExpiredTusUploads(): Promise { + try { + const now = new Date(); + const result = await TusUploadModel.deleteMany({ + expiresAt: { $lt: now }, + }); + if (result.deletedCount > 0) { + logger.info( + `Cleaned up ${result.deletedCount} expired tus uploads`, + ); + } + } catch (err: any) { + logger.error({ err }, "Error cleaning up expired tus uploads"); + } +} + +export async function getTusUploadsByUserId( + userId: string, +): Promise { + return TusUploadModel.find({ userId }).sort({ createdAt: -1 }); +} diff --git a/apps/api/src/tus/routes.ts b/apps/api/src/tus/routes.ts new file mode 100644 index 00000000..e1f26362 --- /dev/null +++ b/apps/api/src/tus/routes.ts @@ -0,0 +1,147 @@ +import express, { Request, Response } from "express"; +import cors from "cors"; +import apikey from "../apikey/middleware"; +import presigned from "../presigning/middleware"; +import { server } from "./tus-server"; +import logger from "../services/log"; +import { EVENTS } from "@tus/server"; +import { createTusUpload, updateTusUploadOffset } from "./queries"; + +const router = express.Router(); + +const authChain = async ( + req: Request & { user?: any; apikey?: string }, + res: Response, + next: () => void, +) => { + // Check for signature in both query params and custom header + const signature = + req.query.signature || + req.headers["x-upload-signature"] || + req.headers["X-Upload-Signature"]; + + try { + if (signature) { + // Ensure signature is properly passed to presigned middleware + req.query.signature = signature; // presigned middleware expects it in query + await presigned( + req as Request & { user: any; apikey: string }, + res, + next, + ); + } else { + // For direct API key usage + await apikey(req, res, next); + } + } catch (error) { + logger.error( + { error, path: req.path, method: req.method }, + "Auth error in TUS request", + ); + res.status(401).json({ error: "Authentication failed" }); + } +}; + +// Apply CORS, auth, and TUS handling for both the base endpoint and file-specific URLs +const handleTusRequest = ( + req: Request & { user?: any; apikey?: string }, + res: Response, +) => { + if (!req.user || !req.apikey) { + logger.error("Missing user or apikey in request"); + return res.status(401).json({ error: "Authentication required" }); + } + + if (req.method === "PATCH") { + req.setTimeout(0); // No timeout for uploads + } + + server.on(EVENTS.POST_CREATE, async (tusReq: any, upload: any) => { + // Get user and apikey from request (set by middleware) + const user = req.user; + const apikey = req.apikey; + // const signature = req.signature; + // const metadata = req.tusMetadata || {}; + const metadata = upload.metadata || {}; + console.log(metadata); + + logger.info( + { + uploadId: upload.id, + user, + apikey, + // signature, + // metadata, + uploadSize: upload.size, + }, + "TUS onCreate event", + ); + + if (!user || !apikey) { + logger.error("Missing user or apikey in tus onCreate"); + return; + } + + createTusUpload({ + uploadId: upload.id, + userId: user.id, + apikey, + uploadLength: upload.size, + metadata: { + fileName: metadata.fileName || "unknown", + mimeType: metadata.mimeType || "application/octet-stream", + accessControl: metadata.access, + caption: metadata.caption, + group: metadata.group || (req.body?.group as string), + }, + // signature, + tempFilePath: upload.id, + }).catch((err: any) => { + logger.error({ err }, "Error creating tus upload record"); + }); + }); + + server.on(EVENTS.POST_RECEIVE, async (tusReq: any, upload: any) => { + if (req.method === "PATCH" && upload) { + try { + // tus server updates the offset internally before this hook fires + const offsetHeader = res.getHeader("Upload-Offset"); + const offset = offsetHeader + ? parseInt(String(offsetHeader), 10) + : upload.offset; + console.log( + "Updating offset to:", + offset, + "for upload ID:", + upload.id, + ); + + await updateTusUploadOffset(upload.id, offset); + + logger.info( + { uploadId: upload.id, offset }, + "Updated upload offset in DB", + ); + } catch (err) { + logger.error({ err }, "Failed to update tus upload offset"); + } + } + }); + + server.handle(req, res); +}; + +// Handle the base TUS endpoint +// Parse raw body for PATCH requests +// router.all("/create/tus*", (req: Request, res: Response, next: () => void) => { +// if (req.method === "PATCH") { +// // Ensure body parsing is skipped for PATCH +// console.log("Came here"); +// (req as any).bodyParsed = true; +// } +// next(); +// }); + +router.all("/create/tus*", cors(), authChain, handleTusRequest); + +export default router; diff --git a/apps/api/src/tus/tus-server.ts b/apps/api/src/tus/tus-server.ts new file mode 100644 index 00000000..decaa13b --- /dev/null +++ b/apps/api/src/tus/tus-server.ts @@ -0,0 +1,227 @@ +import { Server } from "@tus/server"; +import { FileStore } from "@tus/file-store"; +import { tempFileDirForUploads, TUS_CHUNK_SIZE } from "../config/constants"; +import logger from "../services/log"; +import { createTusUpload, updateTusUploadOffset } from "./queries"; +import finalizeUpload from "./finalize"; + +const store = new FileStore({ + directory: `${tempFileDirForUploads}/tus-uploads`, +}); + +export const server = new Server({ + path: "/media/create/tus", + datastore: store, + // Use absolute URLs so PATCH requests work with the same base URL + // respectForwardedHeaders: false, + // relativeLocation: false, + // async onIncomingRequest(req: any) { + // // Extract metadata from Upload-Metadata header per TUS spec + // // Format: Key1 Base64(Value1),Key2 Base64(Value2) + // const uploadMetadataHeader = req.headers["upload-metadata"] || req.headers["Upload-Metadata"]; + // console.log(uploadMetadataHeader) + // if (uploadMetadataHeader) { + // try { + // // header may be an array or a string + // const header = Array.isArray(uploadMetadataHeader) + // ? uploadMetadataHeader.join(",") + // : String(uploadMetadataHeader); + + // const metadata = header + // .split(",") + // .map((s) => s.trim()) + // .filter(Boolean) + // .reduce((acc, item) => { + // const parts = item.split(/\s+/); + // const key = parts[0]; + // const valueB64 = parts.slice(1).join(" "); + // if (!key || !valueB64) return acc; + // try { + // acc[key] = Buffer.from(valueB64, "base64").toString("utf8"); + // } catch (e) { + // // If value isn't valid base64, store raw value + // acc[key] = valueB64; + // } + // return acc; + // }, {} as Record); + + // // Store metadata in request for later use + // req.tusMetadata = metadata; + // } catch (err) { + // logger.error({ err }, "Error parsing upload metadata"); + // } + // } + // }, + // async onUploadCreate(req: any, upload: any) { + // console.log(req) + // // Get user and apikey from request (set by middleware) + // const user = req.user; + // const apikey = req.apikey; + // const signature = req.signature; + // // const metadata = req.tusMetadata || {}; + + // logger.info({ + // uploadId: upload.id, + // user, + // apikey, + // signature, + // // metadata, + // uploadSize: upload.size, + // }, "TUS onCreate event"); + + // if (!user || !apikey) { + // logger.error("Missing user or apikey in tus onCreate"); + // return; + // } + + // createTusUpload({ + // userId: user.id, + // apikey, + // uploadLength: upload.size, + // metadata: { + // // filename: metadata.filename || "unknown", + // // mimetype: metadata.mimetype || "application/octet-stream", + // // access: metadata.access, + // // caption: metadata.caption, + // // group: metadata.group || (req.body?.group as string), + // }, + // signature, + // tempFilePath: upload.id, + // }).catch((err) => { + // logger.error({ err }, "Error creating tus upload record"); + // }); + + // return { + // metadata: { + // ...upload.metadata, + // }, + // } as any; + // } +}); + +// Set up event handlers +// server.on("onRequest", async (req: any, res: any) => { +// logger.info({ +// method: req.method, +// url: req.url, +// headers: req.headers, +// }, "TUS server received request"); +// }); + +// server.on("onIncomingRequest", async (req: any) => { +// // Extract metadata from Upload-Metadata header per TUS spec +// // Format: Key1 Base64(Value1),Key2 Base64(Value2) +// const uploadMetadataHeader = req.headers["upload-metadata"] || req.headers["Upload-Metadata"]; +// if (uploadMetadataHeader) { +// try { +// // header may be an array or a string +// const header = Array.isArray(uploadMetadataHeader) +// ? uploadMetadataHeader.join(",") +// : String(uploadMetadataHeader); + +// const metadata = header +// .split(",") +// .map((s) => s.trim()) +// .filter(Boolean) +// .reduce((acc, item) => { +// const parts = item.split(/\s+/); +// const key = parts[0]; +// const valueB64 = parts.slice(1).join(" "); +// if (!key || !valueB64) return acc; +// try { +// acc[key] = Buffer.from(valueB64, "base64").toString("utf8"); +// } catch (e) { +// // If value isn't valid base64, store raw value +// acc[key] = valueB64; +// } +// return acc; +// }, {} as Record); + +// // Store metadata in request for later use +// req.tusMetadata = metadata; +// } catch (err) { +// logger.error({ err }, "Error parsing upload metadata"); +// } +// } +// }); + +// server.on("onCreate", async (req: any, upload: any) => { +// // Get user and apikey from request (set by middleware) +// const user = req.user; +// const apikey = req.apikey; +// const signature = req.signature; +// const metadata = req.tusMetadata || {}; + +// logger.info({ +// uploadId: upload.id, +// user, +// apikey, +// signature, +// metadata, +// uploadSize: upload.size, +// }, "TUS onCreate event"); + +// if (!user || !apikey) { +// logger.error("Missing user or apikey in tus onCreate"); +// return; +// } + +// createTusUpload({ +// userId: user.id, +// apikey, +// uploadLength: upload.size, +// metadata: { +// filename: metadata.filename || "unknown", +// mimetype: metadata.mimetype || "application/octet-stream", +// access: metadata.access, +// caption: metadata.caption, +// group: metadata.group || (req.body?.group as string), +// }, +// signature, +// tempFilePath: upload.id, +// }).catch((err) => { +// logger.error({ err }, "Error creating tus upload record"); +// }); +// }); + +server.on("onResponse", async (req: any, res: any, upload: any) => { + if (req.method === "PATCH" && upload) { + try { + // tus server updates the offset internally before this hook fires + const offsetHeader = res.getHeader("Upload-Offset"); + const offset = offsetHeader + ? parseInt(String(offsetHeader), 10) + : upload.offset; + + await updateTusUploadOffset(upload.id, offset); + + logger.info( + { uploadId: upload.id, offset }, + "Updated upload offset in DB", + ); + } catch (err) { + logger.error({ err }, "Failed to update tus upload offset"); + } + } +}); + +server.on("onUploadFinish", async (req: any, upload: any) => { + logger.info( + { + uploadId: upload.id, + offset: upload.offset, + size: upload.size, + metadata: upload.metadata, + }, + "Tus upload finished (onUploadFinish)", + ); + + // Finalize the upload (move to S3, generate thumbnails, etc.) + finalizeUpload(upload.id).catch((err) => { + logger.error( + { err, uploadId: upload.id }, + "Error finalizing tus upload", + ); + }); + return {}; +}); diff --git a/examples/next-app-router/README.md b/examples/next-app-router/README.md index f61f3d36..b8c48fc4 100644 --- a/examples/next-app-router/README.md +++ b/examples/next-app-router/README.md @@ -1,5 +1,23 @@ This is a [Next.js](https://nextjs.org) project which demonstrates the usage of `medialit` package. +## Features + +This example app demonstrates two upload methods: + +1. **Standard Upload** - Traditional single-request upload, best for smaller files +2. **TUS Resumable Upload** - Multipart resumable uploads using the TUS protocol with: + - Real-time upload progress tracking + - Automatic retry on failure + - Cancel upload capability + - Ideal for larger files and unreliable connections + +Additional features: + +- Automatic thumbnail generation +- Media information retrieval +- File deletion +- Media listing with pagination + ## Getting Started First, add the MediaLit API key to `.env` file in the root directory: @@ -8,6 +26,12 @@ First, add the MediaLit API key to `.env` file in the root directory: MEDIALIT_API_KEY=your_api_key_here ``` +Optionally, if you're running a self-hosted instance, set the endpoint: + +```env +MEDIALIT_ENDPOINT=http://localhost:3001 +``` + Then, run the development server: ```bash diff --git a/examples/next-app-router/app/api/medialit/tus/route.ts b/examples/next-app-router/app/api/medialit/tus/route.ts new file mode 100644 index 00000000..ff8a1db8 --- /dev/null +++ b/examples/next-app-router/app/api/medialit/tus/route.ts @@ -0,0 +1,33 @@ +import { MediaLit } from "medialit"; + +const client = new MediaLit(); +const endpoint = process.env.MEDIALIT_ENDPOINT || "https://api.medialit.cloud"; + +export async function POST() { + try { + // Get presigned URL from SDK - it returns the full URL + const presignedUrl = await client.getPresignedUploadUrl(); + + // Extract signature from the URL (URL format: http://host/media/create?signature=xxx) + const url = new URL(presignedUrl); + const signature = url.searchParams.get("signature"); + + if (!signature) { + return Response.json( + { error: "Failed to extract signature from URL" }, + { status: 500 }, + ); + } + + return Response.json({ signature, endpoint }); + } catch (error) { + if (error instanceof Error) { + console.log("Error getting presigned signature:", error); + return Response.json({ error: error.message }, { status: 500 }); + } + return Response.json( + { error: "An unknown error occurred" }, + { status: 500 }, + ); + } +} diff --git a/examples/next-app-router/app/page.tsx b/examples/next-app-router/app/page.tsx index a2c7df79..ad1a7e32 100644 --- a/examples/next-app-router/app/page.tsx +++ b/examples/next-app-router/app/page.tsx @@ -1,10 +1,12 @@ +"use client"; + import MediaUploadForm from "@/components/MediaUploadForm"; +import TusUploadForm from "@/components/TusUploadForm"; import MediaList from "@/components/MediaList"; - export default function Home() { return (
-
+

MediaLit Demo

@@ -12,14 +14,45 @@ export default function Home() { handle media files. Upload an image to see:

    -
  • Direct upload using presigned URLs
  • +
  • + Regular upload and resumable upload using TUS + protocol +
  • +
  • Real-time upload progress tracking
  • Automatic thumbnail generation
  • Media information retrieval
  • File deletion
  • Media listing with pagination
- + + {/* Upload Forms Grid */} +
+
+
+

+ Standard Upload +

+

+ Traditional single-request upload. Best for + smaller files. +

+ +
+
+

+ TUS Resumable Upload +

+

+ Multipart resumable uploads with progress + tracking. Ideal for larger files and unreliable + connections. +

+ +
+
+
+

Uploaded Media

diff --git a/examples/next-app-router/components/TusUploadForm.tsx b/examples/next-app-router/components/TusUploadForm.tsx new file mode 100644 index 00000000..21e2ed42 --- /dev/null +++ b/examples/next-app-router/components/TusUploadForm.tsx @@ -0,0 +1,264 @@ +"use client"; + +import { useState, useRef } from "react"; +import { Upload } from "tus-js-client"; + +interface Media { + mediaId: string; + originalFileName: string; + mimeType: string; + size: number; + access: string; + file: string; + group?: string; + caption?: string; + thumbnail: string; +} + +export default function TusUploadForm() { + const [file, setFile] = useState(null); + const [uploading, setUploading] = useState(false); + const [uploadedMedia, setUploadedMedia] = useState(null); + const [error, setError] = useState(""); + const [caption, setCaption] = useState(""); + const [isPublic, setIsPublic] = useState(false); + const [uploadProgress, setUploadProgress] = useState(0); + const [uploadSpeed, setUploadSpeed] = useState(""); + const uploadRef = useRef(null); + + const handleUpload = async (e: React.FormEvent) => { + e.preventDefault(); + if (!file) return; + + setUploading(true); + setError(""); + setUploadProgress(0); + setUploadSpeed(""); + + try { + // Get signature and endpoint from our API route for tus uploads + const signatureResponse = await fetch("/api/medialit/tus", { + method: "POST", + }); + const { + signature, + endpoint, + error: tusError, + } = await signatureResponse.json(); + + if (tusError || !signature) { + throw new Error( + tusError || "Failed to get signature for tus upload", + ); + } + + // Use the endpoint directly since we're sending signature in headers + const uploadUrl = `${endpoint}/media/create/tus`; + + // Prepare metadata for tus (tus-js-client will encode values as base64) + const metadata = { + fileName: file.name, + mimeType: file.type, + access: isPublic ? "public" : "private", + caption: caption || "", + }; + + // Create tus upload + const upload = new Upload(file, { + endpoint: uploadUrl, + retryDelays: [0, 3000, 5000, 10000, 20000], + headers: { + "X-Upload-Signature": signature, + }, + metadata, + onError: (error) => { + console.error("Tus upload error:", error); + setError( + error instanceof Error + ? error.message + : "Upload failed", + ); + setUploading(false); + }, + onProgress: (bytesUploaded, bytesTotal) => { + const percentage = (bytesUploaded / bytesTotal) * 100; + setUploadProgress(percentage); + }, + onSuccess: async () => { + console.log("Upload finished!"); + setUploading(false); + setUploadProgress(100); + + // Show success message - user can see the uploaded file in the media list below + setUploadedMedia({ + mediaId: "uploaded", + originalFileName: file.name, + mimeType: file.type, + size: file.size, + access: isPublic ? "public" : "private", + file: "", + caption: caption || "", + thumbnail: "", + }); + + // Reset form after 3 seconds + setTimeout(() => { + setFile(null); + setCaption(""); + setUploadedMedia(null); + }, 3000); + }, + }); + + uploadRef.current = upload; + upload.start(); + } catch (err) { + setError( + err instanceof Error + ? err.message + : "An error occurred during upload", + ); + setUploading(false); + } + }; + + const handleCancel = () => { + if (uploadRef.current) { + uploadRef.current.abort(); + uploadRef.current = null; + setUploading(false); + setUploadProgress(0); + } + }; + + return ( +
+
+
+ + setFile(e.target.files?.[0] || null)} + className="border rounded-md p-2" + /> +
+ +
+ + setCaption(e.target.value)} + className="border rounded-md p-2" + /> +
+ +
+ setIsPublic(e.target.checked)} + className="rounded" + /> + +
+ + + + {uploading && ( + + )} +
+ + {uploading && ( +
+
+
+
+

+ {uploadProgress.toFixed(1)}% uploaded +

+ {uploadSpeed && ( +

{uploadSpeed}

+ )} +
+ )} + + {error && ( +
+ {error} +
+ )} + + {uploadedMedia && ( +
+

+ ✅ Upload Complete! +

+ +
+

+ File name:{" "} + {uploadedMedia.originalFileName} +

+

+ Size:{" "} + {Math.round(uploadedMedia.size / 1024)} KB +

+

+ Type:{" "} + {uploadedMedia.mimeType} +

+

+ Access:{" "} + {uploadedMedia.access} +

+ {uploadedMedia.caption && ( +

+ Caption:{" "} + {uploadedMedia.caption} +

+ )} +
+ +

+ Your file has been uploaded successfully! Check the + media list below to see it with all details including + the thumbnail. +

+
+ )} +
+ ); +} diff --git a/examples/next-app-router/package.json b/examples/next-app-router/package.json index d8327f85..395cb0e5 100644 --- a/examples/next-app-router/package.json +++ b/examples/next-app-router/package.json @@ -13,7 +13,8 @@ "medialit": "workspace:^", "next": "15.3.1", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "tus-js-client": "^4.3.1" }, "devDependencies": { "@eslint/eslintrc": "^3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9a8179dc..48e7f652 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -71,6 +71,12 @@ importers: '@medialit/utils': specifier: workspace:^0.1.0 version: link:../../packages/utils + '@tus/file-store': + specifier: ^2.0.0 + version: 2.0.0 + '@tus/server': + specifier: ^2.3.0 + version: 2.3.0 aws-sdk: specifier: ^2.1692.0 version: 2.1692.0 @@ -341,6 +347,9 @@ importers: react-dom: specifier: ^19.0.0 version: 19.1.0(react@19.1.0) + tus-js-client: + specifier: ^4.3.1 + version: 4.3.1 devDependencies: '@eslint/eslintrc': specifier: ^3 @@ -1135,6 +1144,9 @@ packages: cpu: [x64] os: [win32] + '@ioredis/commands@1.4.0': + resolution: {integrity: sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ==} + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -2046,6 +2058,10 @@ packages: '@radix-ui/rect@1.1.1': resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + '@redis/client@1.6.1': + resolution: {integrity: sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==} + engines: {node: '>=14'} + '@rollup/rollup-android-arm-eabi@4.40.0': resolution: {integrity: sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg==} cpu: [arm] @@ -2509,6 +2525,18 @@ packages: '@tsconfig/node16@1.0.4': resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + '@tus/file-store@2.0.0': + resolution: {integrity: sha512-LTh9L/RoWoo2TbBGPZOuhuyEIIqweoTekT77ZkIVkpYkLK8zTt++PRdY+VyJsLDbFMO9RzvKSBRmj1H8SPdDew==} + engines: {node: '>=20.19.0'} + + '@tus/server@2.3.0': + resolution: {integrity: sha512-7sj4Q3EPvMjS5z9JaNOZ8gvT6HZvDeg/RjEP0ebbfpAo1V095ivkzpKqV/mDIK/ioBwOKj+bOhTtNuueTjVCfw==} + engines: {node: '>=20.19.0'} + + '@tus/utils@0.6.0': + resolution: {integrity: sha512-GpMpAQfVdC4UDhpsZrRPjGpdXg+JW5MquqMqtObUVsORwLBV6XI67iTT5be+z98THdqb6dl3bTLIElIdgPeo2g==} + engines: {node: '>=20.19.0'} + '@tybys/wasm-util@0.9.0': resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==} @@ -3318,6 +3346,10 @@ packages: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} + cluster-key-slot@1.1.2: + resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} + engines: {node: '>=0.10.0'} + co@4.6.0: resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} @@ -3352,6 +3384,9 @@ packages: colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + combine-errors@3.0.3: + resolution: {integrity: sha512-C8ikRNRMygCwaTx+Ek3Yr+OuZzgZjduCOfSQBjbM8V3MfgcjSTeto/GXP6PAwKvJz/v15b7GHZvx5rOlczFw/Q==} + combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} @@ -3437,6 +3472,9 @@ packages: csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + custom-error-instance@2.1.1: + resolution: {integrity: sha512-p6JFxJc3M4OTD2li2qaHkDCw9SfMw82Ldr6OC9Je1aXiGfhx2W8p3GaoeaGrPJTUN9NirTM/KTxHWMUdR1rsUg==} + damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} @@ -3522,6 +3560,10 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -4164,6 +4206,10 @@ packages: functions-have-names@1.2.3: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + generic-pool@3.9.0: + resolution: {integrity: sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==} + engines: {node: '>= 4'} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -4423,6 +4469,10 @@ packages: invariant@2.2.4: resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + ioredis@5.8.2: + resolution: {integrity: sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q==} + engines: {node: '>=12.22.0'} + ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} @@ -4599,6 +4649,10 @@ packages: resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==} engines: {node: '>=0.10.0'} + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + is-stream@3.0.0: resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -4831,6 +4885,9 @@ packages: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} + js-base64@3.7.8: + resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -5047,9 +5104,33 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash._baseiteratee@4.7.0: + resolution: {integrity: sha512-nqB9M+wITz0BX/Q2xg6fQ8mLkyfF7MU7eE+MNBNjTHFKeKaZAPEzEg+E8LWxKWf1DQVflNEn9N49yAuqKh2mWQ==} + + lodash._basetostring@4.12.0: + resolution: {integrity: sha512-SwcRIbyxnN6CFEEK4K1y+zuApvWdpQdBHM/swxP962s8HIxPO3alBH5t3m/dl+f4CMUug6sJb7Pww8d13/9WSw==} + + lodash._baseuniq@4.6.0: + resolution: {integrity: sha512-Ja1YevpHZctlI5beLA7oc5KNDhGcPixFhcqSiORHNsp/1QTv7amAXzw+gu4YOvErqVlMVyIJGgtzeepCnnur0A==} + + lodash._createset@4.0.3: + resolution: {integrity: sha512-GTkC6YMprrJZCYU3zcqZj+jkXkrXzq3IPBcF/fIPpNEAB4hZEtXU8zp/RwKOvZl43NUmwDbyRk3+ZTbeRdEBXA==} + + lodash._root@3.0.1: + resolution: {integrity: sha512-O0pWuFSK6x4EXhM1dhZ8gchNtG7JMqBtrHdoUFUWXD7dJnNSUze1GuyQr5sOs0aCvgGeI3o/OJW8f4ca7FDxmQ==} + + lodash._stringtopath@4.8.0: + resolution: {integrity: sha512-SXL66C731p0xPDC5LZg4wI5H+dJo/EO4KTqOMwLYCH3+FmmfAKJEZCm6ohGpI+T1xwsDsJCfL4OnhorllvlTPQ==} + + lodash.defaults@4.2.0: + resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + lodash.includes@4.3.0: resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + lodash.isarguments@3.1.0: + resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} + lodash.isboolean@3.0.3: resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} @@ -5077,6 +5158,12 @@ packages: lodash.startcase@4.4.0: resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + lodash.throttle@4.1.1: + resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==} + + lodash.uniqby@4.5.0: + resolution: {integrity: sha512-IRt7cfTtHy6f1aRVA5n7kT8rgN3N1nH6MOWLcHfpWG2SH19E3JksLK38MktLxZDhlAjCP9jpIXkOnRXlu6oByQ==} + lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} @@ -5942,6 +6029,9 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + proper-lockfile@4.1.2: + resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} + property-information@7.0.0: resolution: {integrity: sha512-7D/qOz/+Y4X/rzSB6jKxKUsQnphO046ei8qxG59mtM3RG3DHgTK81HrxrmoDVINJb8NKT5ZsRbwHvQ6B68Iyhg==} @@ -5981,6 +6071,9 @@ packages: engines: {node: '>=0.4.x'} deprecated: The querystring API is considered Legacy. new code should use the URLSearchParams API instead. + querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -6095,6 +6188,14 @@ packages: recma-stringify@1.0.0: resolution: {integrity: sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==} + redis-errors@1.2.0: + resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} + engines: {node: '>=4'} + + redis-parser@3.0.0: + resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} + engines: {node: '>=4'} + reflect.getprototypeof@1.0.10: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} @@ -6176,6 +6277,9 @@ packages: require-main-filename@2.0.0: resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + resolve-cwd@2.0.0: resolution: {integrity: sha512-ccu8zQTrzVr954472aUVPLEcB3YpKSYR3cg/3lo1okzobPBM+1INXBbBZlDbnI/hbEocnf8j0QVo43hQKrbchg==} engines: {node: '>=4'} @@ -6219,6 +6323,10 @@ packages: resolution: {integrity: sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==} engines: {node: '>=0.12'} + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -6319,6 +6427,9 @@ packages: set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + set-cookie-parser@2.7.1: + resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -6467,6 +6578,7 @@ packages: source-map@0.8.0-beta.0: resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} engines: {node: '>= 8'} + deprecated: The work that was done in this beta branch won't be included in future versions space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} @@ -6500,6 +6612,11 @@ packages: sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + srvx@0.8.16: + resolution: {integrity: sha512-hmcGW4CgroeSmzgF1Ihwgl+Ths0JqAJ7HwjP2X7e3JzY7u4IydLMcdnlqGQiQGUswz+PO9oh/KtCpOISIvs9QQ==} + engines: {node: '>=20.16.0'} + hasBin: true + sshpk@1.18.0: resolution: {integrity: sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==} engines: {node: '>=0.10.0'} @@ -6512,6 +6629,9 @@ packages: resolution: {integrity: sha512-KZiTzuV3CnSnSvgMRrARVCj+Ht7rMbauGDK0LdVFRGyenwdylpajAp4Q0i6SX8rEmbTpMMf6ryq2gb8pPq2WgQ==} engines: {node: '>=8'} + standard-as-callback@2.1.0: + resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + static-extend@0.1.2: resolution: {integrity: sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==} engines: {node: '>=0.10.0'} @@ -6837,6 +6957,10 @@ packages: tunnel-agent@0.6.0: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + tus-js-client@4.3.1: + resolution: {integrity: sha512-ZLeYmjrkaU1fUsKbIi8JML52uAocjEZtBx4DKjRrqzrZa0O4MYwT6db+oqePlspV+FxXJAyFBc/L5gwUi2OFsg==} + engines: {node: '>=18'} + tweetnacl@0.14.5: resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} @@ -6950,6 +7074,9 @@ packages: resolution: {integrity: sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==} deprecated: Please see https://github.com/lydell/urix#deprecated + url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + url@0.10.3: resolution: {integrity: sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==} @@ -7136,6 +7263,9 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + yaml@2.7.1: resolution: {integrity: sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==} engines: {node: '>= 14'} @@ -8214,6 +8344,9 @@ snapshots: '@img/sharp-win32-x64@0.34.1': optional: true + '@ioredis/commands@1.4.0': + optional: true + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -9336,6 +9469,13 @@ snapshots: '@radix-ui/rect@1.1.1': {} + '@redis/client@1.6.1': + dependencies: + cluster-key-slot: 1.1.2 + generic-pool: 3.9.0 + yallist: 4.0.0 + optional: true + '@rollup/rollup-android-arm-eabi@4.40.0': optional: true @@ -9868,6 +10008,30 @@ snapshots: '@tsconfig/node16@1.0.4': {} + '@tus/file-store@2.0.0': + dependencies: + '@tus/utils': 0.6.0 + debug: 4.4.0(supports-color@5.5.0) + optionalDependencies: + '@redis/client': 1.6.1 + transitivePeerDependencies: + - supports-color + + '@tus/server@2.3.0': + dependencies: + '@tus/utils': 0.6.0 + debug: 4.4.0(supports-color@5.5.0) + lodash.throttle: 4.1.1 + set-cookie-parser: 2.7.1 + srvx: 0.8.16 + optionalDependencies: + '@redis/client': 1.6.1 + ioredis: 5.8.2 + transitivePeerDependencies: + - supports-color + + '@tus/utils@0.6.0': {} + '@tybys/wasm-util@0.9.0': dependencies: tslib: 2.8.1 @@ -10819,6 +10983,9 @@ snapshots: clsx@2.1.1: {} + cluster-key-slot@1.1.2: + optional: true + co@4.6.0: {} collapse-white-space@2.1.0: {} @@ -10854,6 +11021,11 @@ snapshots: colorette@2.0.20: {} + combine-errors@3.0.3: + dependencies: + custom-error-instance: 2.1.1 + lodash.uniqby: 4.5.0 + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 @@ -10921,6 +11093,8 @@ snapshots: csstype@3.1.3: {} + custom-error-instance@2.1.1: {} + damerau-levenshtein@1.0.8: {} dashdash@1.14.1: @@ -11002,6 +11176,9 @@ snapshots: delayed-stream@1.0.0: {} + denque@2.1.0: + optional: true + depd@2.0.0: {} dequal@2.0.3: {} @@ -11279,8 +11456,8 @@ snapshots: '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@5.8.3) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0)(eslint@8.57.1) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0)(eslint@8.57.1) eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1) eslint-plugin-react: 7.37.5(eslint@8.57.1) eslint-plugin-react-hooks: 5.2.0(eslint@8.57.1) @@ -11299,8 +11476,8 @@ snapshots: '@typescript-eslint/parser': 5.62.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3) eslint: 9.24.0(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.24.0(jiti@2.4.2)))(eslint@9.24.0(jiti@2.4.2)) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.24.0(jiti@2.4.2)))(eslint@9.24.0(jiti@2.4.2)))(eslint@9.24.0(jiti@2.4.2)) + eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0)(eslint@9.24.0(jiti@2.4.2)) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0)(eslint@9.24.0(jiti@2.4.2)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.24.0(jiti@2.4.2)) eslint-plugin-react: 7.37.5(eslint@9.24.0(jiti@2.4.2)) eslint-plugin-react-hooks: 5.2.0(eslint@9.24.0(jiti@2.4.2)) @@ -11319,7 +11496,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1): + eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0)(eslint@8.57.1): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.0(supports-color@5.5.0) @@ -11330,11 +11507,11 @@ snapshots: tinyglobby: 0.2.12 unrs-resolver: 1.4.1 optionalDependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0)(eslint@8.57.1) transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.24.0(jiti@2.4.2)))(eslint@9.24.0(jiti@2.4.2)): + eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0)(eslint@9.24.0(jiti@2.4.2)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.0(supports-color@5.5.0) @@ -11345,33 +11522,33 @@ snapshots: tinyglobby: 0.2.12 unrs-resolver: 1.4.1 optionalDependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.24.0(jiti@2.4.2)))(eslint@9.24.0(jiti@2.4.2)))(eslint@9.24.0(jiti@2.4.2)) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0)(eslint@9.24.0(jiti@2.4.2)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): + eslint-module-utils@2.12.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0)(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@5.8.3) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1) + eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0)(eslint@8.57.1) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@5.62.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.24.0(jiti@2.4.2)))(eslint@9.24.0(jiti@2.4.2)))(eslint@9.24.0(jiti@2.4.2)): + eslint-module-utils@2.12.0(@typescript-eslint/parser@5.62.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0)(eslint@9.24.0(jiti@2.4.2)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 5.62.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3) eslint: 9.24.0(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.24.0(jiti@2.4.2)))(eslint@9.24.0(jiti@2.4.2)) + eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0)(eslint@9.24.0(jiti@2.4.2)) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0)(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -11382,7 +11559,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0)(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -11400,7 +11577,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.24.0(jiti@2.4.2)))(eslint@9.24.0(jiti@2.4.2)))(eslint@9.24.0(jiti@2.4.2)): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0)(eslint@9.24.0(jiti@2.4.2)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -11411,7 +11588,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.24.0(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@5.62.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.24.0(jiti@2.4.2)))(eslint@9.24.0(jiti@2.4.2)))(eslint@9.24.0(jiti@2.4.2)) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@5.62.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0)(eslint@9.24.0(jiti@2.4.2)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -12066,6 +12243,9 @@ snapshots: functions-have-names@1.2.3: {} + generic-pool@3.9.0: + optional: true + gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} @@ -12367,6 +12547,21 @@ snapshots: dependencies: loose-envify: 1.4.0 + ioredis@5.8.2: + dependencies: + '@ioredis/commands': 1.4.0 + cluster-key-slot: 1.1.2 + debug: 4.4.0(supports-color@5.5.0) + denque: 2.1.0 + lodash.defaults: 4.2.0 + lodash.isarguments: 3.1.0 + redis-errors: 1.2.0 + redis-parser: 3.0.0 + standard-as-callback: 2.1.0 + transitivePeerDependencies: + - supports-color + optional: true + ipaddr.js@1.9.1: {} is-accessor-descriptor@1.0.1: @@ -12533,6 +12728,8 @@ snapshots: is-stream@1.1.0: {} + is-stream@2.0.1: {} + is-stream@3.0.0: {} is-string@1.1.1: @@ -12767,7 +12964,9 @@ snapshots: pretty-format: 24.9.0 throat: 4.1.0 transitivePeerDependencies: + - bufferutil - supports-color + - utf-8-validate jest-leak-detector@24.9.0: dependencies: @@ -12966,6 +13165,8 @@ snapshots: joycon@3.1.1: {} + js-base64@3.7.8: {} + js-tokens@4.0.0: {} js-yaml@3.14.1: @@ -13206,8 +13407,33 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash._baseiteratee@4.7.0: + dependencies: + lodash._stringtopath: 4.8.0 + + lodash._basetostring@4.12.0: {} + + lodash._baseuniq@4.6.0: + dependencies: + lodash._createset: 4.0.3 + lodash._root: 3.0.1 + + lodash._createset@4.0.3: {} + + lodash._root@3.0.1: {} + + lodash._stringtopath@4.8.0: + dependencies: + lodash._basetostring: 4.12.0 + + lodash.defaults@4.2.0: + optional: true + lodash.includes@4.3.0: {} + lodash.isarguments@3.1.0: + optional: true + lodash.isboolean@3.0.3: {} lodash.isinteger@4.0.4: {} @@ -13226,6 +13452,13 @@ snapshots: lodash.startcase@4.4.0: {} + lodash.throttle@4.1.1: {} + + lodash.uniqby@4.5.0: + dependencies: + lodash._baseiteratee: 4.7.0 + lodash._baseuniq: 4.6.0 + lodash@4.17.21: {} log-update@6.1.0: @@ -14332,6 +14565,12 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + proper-lockfile@4.1.2: + dependencies: + graceful-fs: 4.2.11 + retry: 0.12.0 + signal-exit: 3.0.7 + property-information@7.0.0: {} proxy-addr@2.0.7: @@ -14364,6 +14603,8 @@ snapshots: querystring@0.2.0: {} + querystringify@2.2.0: {} + queue-microtask@1.2.3: {} quick-format-unescaped@4.0.4: {} @@ -14519,6 +14760,14 @@ snapshots: unified: 11.0.5 vfile: 6.0.3 + redis-errors@1.2.0: + optional: true + + redis-parser@3.0.0: + dependencies: + redis-errors: 1.2.0 + optional: true + reflect.getprototypeof@1.0.10: dependencies: call-bind: 1.0.8 @@ -14659,6 +14908,8 @@ snapshots: require-main-filename@2.0.0: {} + requires-port@1.0.0: {} + resolve-cwd@2.0.0: dependencies: resolve-from: 3.0.0 @@ -14694,6 +14945,8 @@ snapshots: ret@0.1.15: {} + retry@0.12.0: {} + reusify@1.1.0: {} rfdc@1.4.1: {} @@ -14831,6 +15084,8 @@ snapshots: set-blocking@2.0.0: {} + set-cookie-parser@2.7.1: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -15066,6 +15321,8 @@ snapshots: sprintf-js@1.0.3: {} + srvx@0.8.16: {} + sshpk@1.18.0: dependencies: asn1: 0.2.6 @@ -15084,6 +15341,9 @@ snapshots: dependencies: escape-string-regexp: 2.0.0 + standard-as-callback@2.1.0: + optional: true + static-extend@0.1.2: dependencies: define-property: 0.2.5 @@ -15471,6 +15731,16 @@ snapshots: dependencies: safe-buffer: 5.2.1 + tus-js-client@4.3.1: + dependencies: + buffer-from: 1.1.2 + combine-errors: 3.0.3 + is-stream: 2.0.1 + js-base64: 3.7.8 + lodash.throttle: 4.1.1 + proper-lockfile: 4.1.2 + url-parse: 1.5.10 + tweetnacl@0.14.5: {} type-check@0.3.2: @@ -15629,6 +15899,11 @@ snapshots: urix@0.1.0: {} + url-parse@1.5.10: + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + url@0.10.3: dependencies: punycode: 1.3.2 @@ -15861,6 +16136,9 @@ snapshots: yallist@3.1.1: {} + yallist@4.0.0: + optional: true + yaml@2.7.1: {} yargs-parser@13.1.2: From 6aabf80bcc6eebee7312805f42f4ecd501e3ea68 Mon Sep 17 00:00:00 2001 From: Rajat Saxena Date: Thu, 30 Oct 2025 09:27:26 +0530 Subject: [PATCH 2/8] tested: ~380mb video upload; removed boilerplate code from tus feature; x-medialit-signature and x-medialit-apikey headers introduced --- apps/api/package.json | 2 +- apps/api/src/apikey/middleware.ts | 15 +- apps/api/src/config/constants.ts | 8 +- apps/api/src/config/strings.ts | 2 + apps/api/src/index.ts | 1 - apps/api/src/media/storage-middleware.ts | 31 +- apps/api/src/presigning/middleware.ts | 5 +- apps/api/src/presigning/service.ts | 2 +- apps/api/src/tus/finalize.ts | 107 +++--- apps/api/src/tus/queries.ts | 6 - apps/api/src/tus/routes.ts | 118 +----- apps/api/src/tus/tus-server.ts | 338 +++++++----------- .../next-app-router/components/MediaList.tsx | 42 ++- .../components/TusUploadForm.tsx | 6 +- pnpm-lock.yaml | 304 ++++++++-------- 15 files changed, 424 insertions(+), 563 deletions(-) diff --git a/apps/api/package.json b/apps/api/package.json index 172c1886..5c78f281 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -43,7 +43,7 @@ "aws-sdk": "^2.1692.0", "cors": "^2.8.5", "dotenv": "^16.4.7", - "express": "^4.18.2", + "express": "^5.1.0", "express-fileupload": "^1.3.1", "joi": "^17.6.0", "mongoose": "^8.0.1", diff --git a/apps/api/src/apikey/middleware.ts b/apps/api/src/apikey/middleware.ts index 4fe8e2b7..7347de33 100644 --- a/apps/api/src/apikey/middleware.ts +++ b/apps/api/src/apikey/middleware.ts @@ -1,8 +1,4 @@ -import { - BAD_REQUEST, - SUBSCRIPTION_NOT_VALID, - UNAUTHORISED, -} from "../config/strings"; +import { BAD_REQUEST, UNAUTHORISED } from "../config/strings"; import { getApiKeyUsingKeyId } from "./queries"; import { getUser } from "../user/queries"; import { Apikey } from "@medialit/models"; @@ -12,7 +8,7 @@ export default async function apikey( res: any, next: (...args: any[]) => void, ) { - const reqKey = req.body.apikey; + const reqKey = req.body.apikey || req.headers["x-medialit-apikey"]; if (!reqKey) { console.log("API key missing in request"); @@ -24,13 +20,6 @@ export default async function apikey( return res.status(401).json({ error: UNAUTHORISED }); } - // const isSubscriptionValid = await validateSubscription( - // apiKey!.userId.toString(), - // ); - // if (!isSubscriptionValid) { - // return res.status(403).json({ error: SUBSCRIPTION_NOT_VALID }); - // } - req.user = await getUser(apiKey!.userId.toString()); req.apikey = apiKey.key; diff --git a/apps/api/src/config/constants.ts b/apps/api/src/config/constants.ts index 24829137..217e5ce8 100644 --- a/apps/api/src/config/constants.ts +++ b/apps/api/src/config/constants.ts @@ -6,19 +6,19 @@ export const tempFileDirForUploads = process.env.TEMP_FILE_DIR_FOR_UPLOADS; export const maxFileUploadSizeSubscribed = process.env .MAX_UPLOAD_SIZE_SUBSCRIBED ? +process.env.MAX_UPLOAD_SIZE_SUBSCRIBED - : 2147483648; + : 2147483648; // 2GB export const maxFileUploadSizeNotSubscribed = process.env .MAX_UPLOAD_SIZE_NOT_SUBSCRIBED ? +process.env.MAX_UPLOAD_SIZE_NOT_SUBSCRIBED - : 52428800; + : 52428800; // 50MB export const maxStorageAllowedSubscribed = process.env .MAX_STORAGE_ALLOWED_SUBSCRIBED ? +process.env.MAX_STORAGE_ALLOWED_SUBSCRIBED - : 107374182400; + : 107374182400; // 100GB export const maxStorageAllowedNotSubscribed = process.env .MAX_STORAGE_ALLOWED_NOT_SUBSCRIBED ? +process.env.MAX_STORAGE_ALLOWED_NOT_SUBSCRIBED - : 1073741824; + : 1073741824; // 1GB export const PRESIGNED_URL_VALIDITY_MINUTES = 5; export const PRESIGNED_URL_LENGTH = 100; export const MEDIA_ID_LENGTH = 40; diff --git a/apps/api/src/config/strings.ts b/apps/api/src/config/strings.ts index cecdf848..e0b4372f 100644 --- a/apps/api/src/config/strings.ts +++ b/apps/api/src/config/strings.ts @@ -6,3 +6,5 @@ export const FILE_IS_REQUIRED = "File is required"; export const FILE_SIZE_EXCEEDED = "File size exceeded"; export const NOT_FOUND = "Not found"; export const PRESIGNED_URL_INVALID = "The link is invalid"; +export const NOT_ENOUGH_STORAGE = + "Not enough storage space in your account to upload this file"; diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index e885f6be..5417594b 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -33,7 +33,6 @@ app.use("/settings/media", mediaSettingsRoutes(passport)); app.use("/media/presigned", presignedUrlRoutes); app.use("/media", tusRoutes); app.use("/media", mediaRoutes); -// Mount TUS routes under /media so paths like /media/create/tus match correctly const port = process.env.PORT || 80; diff --git a/apps/api/src/media/storage-middleware.ts b/apps/api/src/media/storage-middleware.ts index 09de214a..6a918474 100644 --- a/apps/api/src/media/storage-middleware.ts +++ b/apps/api/src/media/storage-middleware.ts @@ -1,7 +1,8 @@ import { maxStorageAllowedNotSubscribed } from "../config/constants"; import { maxStorageAllowedSubscribed } from "../config/constants"; -import { Constants, getSubscriptionStatus } from "@medialit/models"; +import { getSubscriptionStatus, User } from "@medialit/models"; import mediaQueries from "./queries"; +import { NOT_ENOUGH_STORAGE } from "../config/strings"; export default async function storageValidation( req: any, @@ -14,21 +15,25 @@ export default async function storageValidation( }); } + if (!(await hasEnoughStorage((req.files.file as any).size, req.user))) { + return res.status(403).json({ + error: NOT_ENOUGH_STORAGE, + }); + } + + next(); +} + +export async function hasEnoughStorage( + size: number, + user: User, +): Promise { const totalSpaceOccupied = await mediaQueries.getTotalSpace({ - userId: (req as any).user.id, + userId: user.id, }); - const maxStorageAllowed = getSubscriptionStatus(req.user) + const maxStorageAllowed = getSubscriptionStatus(user) ? maxStorageAllowedSubscribed : maxStorageAllowedNotSubscribed; - if ( - totalSpaceOccupied + (req.files?.file as any).size > - maxStorageAllowed - ) { - return res.status(400).json({ - error: "You do not have enough storage space in your account to upload this file", - }); - } - - next(); + return totalSpaceOccupied + size <= maxStorageAllowed; } diff --git a/apps/api/src/presigning/middleware.ts b/apps/api/src/presigning/middleware.ts index 0490fe79..ab5f4825 100644 --- a/apps/api/src/presigning/middleware.ts +++ b/apps/api/src/presigning/middleware.ts @@ -3,11 +3,12 @@ import { PRESIGNED_URL_INVALID } from "../config/strings"; import * as preSignedUrlService from "./service"; export default async function presigned( - req: Request & { user: any; apikey: string }, + req: Request & { user?: any; apikey?: string }, res: Response, next: (...args: any[]) => void, ) { - const { signature } = req.query; + const signature = + req.query.signature || req.headers["x-medialit-signature"]; const response = await preSignedUrlService.getUserAndGroupFromPresignedUrl( signature as string, diff --git a/apps/api/src/presigning/service.ts b/apps/api/src/presigning/service.ts index 4577e353..6cad6d79 100644 --- a/apps/api/src/presigning/service.ts +++ b/apps/api/src/presigning/service.ts @@ -90,5 +90,5 @@ export async function generateSignedUrlForTus({ ); }); - return `${protocol}://${host}/media/create/tus?signature=${presignedUrl?.signature}`; + return `${protocol}://${host}/media/create/resumable?signature=${presignedUrl?.signature}`; } diff --git a/apps/api/src/tus/finalize.ts b/apps/api/src/tus/finalize.ts index d72aff77..290999aa 100644 --- a/apps/api/src/tus/finalize.ts +++ b/apps/api/src/tus/finalize.ts @@ -24,44 +24,9 @@ import { createMedia } from "../media/queries"; import getTags from "../media/utils/get-tags"; import { getTusUpload, markTusUploadComplete } from "./queries"; import * as presignedUrlService from "../presigning/service"; - -const generateAndUploadThumbnail = async ({ - workingDirectory, - key, - mimetype, - originalFilePath, - tags, -}: { - workingDirectory: string; - key: string; - mimetype: string; - originalFilePath: string; - tags: string; -}): Promise => { - const thumbPath = `${workingDirectory}/thumb.webp`; - - let isThumbGenerated = false; - if (imagePatternForThumbnailGeneration.test(mimetype)) { - await thumbnail.forImage(originalFilePath, thumbPath); - isThumbGenerated = true; - } - if (videoPattern.test(mimetype)) { - await thumbnail.forVideo(originalFilePath, thumbPath); - isThumbGenerated = true; - } - - if (isThumbGenerated) { - await putObject({ - Key: key, - Body: createReadStream(thumbPath), - ContentType: "image/webp", - ACL: USE_CLOUDFRONT ? "private" : "public-read", - Tagging: tags, - }); - } - - return isThumbGenerated; -}; +import { getUser } from "../user/queries"; +import { hasEnoughStorage } from "../media/storage-middleware"; +import { NOT_ENOUGH_STORAGE } from "../config/strings"; export default async function finalizeUpload(uploadId: string) { logger.info({ uploadId }, "Finalizing tus upload"); @@ -79,6 +44,11 @@ export default async function finalizeUpload(uploadId: string) { const { userId, apikey, metadata, uploadLength, tempFilePath, signature } = tusUpload; + const user = await getUser(userId); + if (!(await hasEnoughStorage(uploadLength, user!))) { + throw new Error(NOT_ENOUGH_STORAGE); + } + // Read the completed file from tus data store const tusFilePath = path.join( `${tempFileDirForUploads}/tus-uploads`, @@ -95,14 +65,14 @@ export default async function finalizeUpload(uploadId: string) { const webpOutputQuality = mediaSettings?.webpOutputQuality || 0; // Generate unique media ID - const fileName = generateFileName(metadata.filename); + const fileName = generateFileName(metadata.fileName); const temporaryFolderForWork = `${tempFileDirForUploads}/${fileName.name}`; if (!foldersExist([temporaryFolderForWork])) { createFolders([temporaryFolderForWork]); } - let fileExtension = path.extname(metadata.filename).replace(".", ""); - let mimeType = metadata.mimetype; + let fileExtension = path.extname(metadata.fileName).replace(".", ""); + let mimeType = metadata.mimeType; if (useWebP && imagePattern.test(mimeType)) { fileExtension = "webp"; mimeType = "image/webp"; @@ -110,26 +80,28 @@ export default async function finalizeUpload(uploadId: string) { const mainFilePath = `${temporaryFolderForWork}/main.${fileExtension}`; - // Copy file from tus store to working directory + console.log(mainFilePath, tusFilePath); + + //Copy file from tus store to working directory const tusFileContent = readFileSync(tusFilePath); require("fs").writeFileSync(mainFilePath, tusFileContent); // Apply WebP conversion if needed - if (useWebP && imagePattern.test(metadata.mimetype)) { + if (useWebP && imagePattern.test(metadata.mimeType)) { await imageUtils.convertToWebp(mainFilePath, webpOutputQuality); } const uploadParams: UploadParams = { Key: generateKey({ mediaId: fileName.name, - access: metadata.access === "public" ? "public" : "private", + access: metadata.accessControl === "public" ? "public" : "private", filename: `main.${fileExtension}`, }), Body: createReadStream(mainFilePath), ContentType: mimeType, ACL: USE_CLOUDFRONT ? "private" - : metadata.access === "public" + : metadata.accessControl === "public" ? "public-read" : "private", }; @@ -142,7 +114,7 @@ export default async function finalizeUpload(uploadId: string) { try { isThumbGenerated = await generateAndUploadThumbnail({ workingDirectory: temporaryFolderForWork, - mimetype: metadata.mimetype, + mimetype: metadata.mimeType, originalFilePath: mainFilePath, key: generateKey({ mediaId: fileName.name, @@ -162,12 +134,13 @@ export default async function finalizeUpload(uploadId: string) { mediaId: fileName.name, userId: new mongoose.Types.ObjectId(userId), apikey, - originalFileName: metadata.filename, + originalFileName: metadata.fileName, mimeType, size: uploadLength, thumbnailGenerated: isThumbGenerated, caption: metadata.caption, - accessControl: metadata.access === "public" ? "public-read" : "private", + accessControl: + metadata.accessControl === "public" ? "public-read" : "private", group: metadata.group, }; const media = await createMedia(mediaObject); @@ -201,3 +174,41 @@ export default async function finalizeUpload(uploadId: string) { return media.mediaId; } + +const generateAndUploadThumbnail = async ({ + workingDirectory, + key, + mimetype, + originalFilePath, + tags, +}: { + workingDirectory: string; + key: string; + mimetype: string; + originalFilePath: string; + tags: string; +}): Promise => { + const thumbPath = `${workingDirectory}/thumb.webp`; + + let isThumbGenerated = false; + if (imagePatternForThumbnailGeneration.test(mimetype)) { + await thumbnail.forImage(originalFilePath, thumbPath); + isThumbGenerated = true; + } + if (videoPattern.test(mimetype)) { + await thumbnail.forVideo(originalFilePath, thumbPath); + isThumbGenerated = true; + } + + if (isThumbGenerated) { + await putObject({ + Key: key, + Body: createReadStream(thumbPath), + ContentType: "image/webp", + ACL: USE_CLOUDFRONT ? "private" : "public-read", + Tagging: tags, + }); + } + + return isThumbGenerated; +}; diff --git a/apps/api/src/tus/queries.ts b/apps/api/src/tus/queries.ts index bf68a6f2..2744e0c1 100644 --- a/apps/api/src/tus/queries.ts +++ b/apps/api/src/tus/queries.ts @@ -25,12 +25,6 @@ export async function createTusUpload( isComplete: false, expiresAt, }; - console.log( - "Creating TusUpload with ID:", - data.uploadId, - data, - tusUploadData, - ); const tusUpload = await TusUploadModel.create(tusUploadData); return tusUpload; diff --git a/apps/api/src/tus/routes.ts b/apps/api/src/tus/routes.ts index e1f26362..ed1ab96f 100644 --- a/apps/api/src/tus/routes.ts +++ b/apps/api/src/tus/routes.ts @@ -6,6 +6,8 @@ import { server } from "./tus-server"; import logger from "../services/log"; import { EVENTS } from "@tus/server"; import { createTusUpload, updateTusUploadOffset } from "./queries"; +import { hasEnoughStorage } from "../media/storage-middleware"; +import { NOT_ENOUGH_STORAGE } from "../config/strings"; const router = express.Router(); @@ -14,134 +16,38 @@ const authChain = async ( res: Response, next: () => void, ) => { - // Check for signature in both query params and custom header const signature = - req.query.signature || - req.headers["x-upload-signature"] || - req.headers["X-Upload-Signature"]; + req.query.signature || req.headers["x-medialit-signature"]; - try { - if (signature) { - // Ensure signature is properly passed to presigned middleware - req.query.signature = signature; // presigned middleware expects it in query - await presigned( - req as Request & { user: any; apikey: string }, - res, - next, - ); - } else { - // For direct API key usage - await apikey(req, res, next); - } - } catch (error) { - logger.error( - { error, path: req.path, method: req.method }, - "Auth error in TUS request", - ); - res.status(401).json({ error: "Authentication failed" }); + if (signature) { + presigned(req, res, next); + } else { + apikey(req, res, next); } }; -// Apply CORS, auth, and TUS handling for both the base endpoint and file-specific URLs const handleTusRequest = ( req: Request & { user?: any; apikey?: string }, res: Response, ) => { - if (!req.user || !req.apikey) { - logger.error("Missing user or apikey in request"); - return res.status(401).json({ error: "Authentication required" }); - } - + console.log("HandleTusRequest", req.method); if (req.method === "PATCH") { req.setTimeout(0); // No timeout for uploads } - server.on(EVENTS.POST_CREATE, async (tusReq: any, upload: any) => { - // Get user and apikey from request (set by middleware) - const user = req.user; - const apikey = req.apikey; - // const signature = req.signature; - // const metadata = req.tusMetadata || {}; - const metadata = upload.metadata || {}; - console.log(metadata); - - logger.info( - { - uploadId: upload.id, - user, - apikey, - // signature, - // metadata, - uploadSize: upload.size, - }, - "TUS onCreate event", - ); - - if (!user || !apikey) { - logger.error("Missing user or apikey in tus onCreate"); - return; - } - - createTusUpload({ - uploadId: upload.id, - userId: user.id, - apikey, - uploadLength: upload.size, - metadata: { - fileName: metadata.fileName || "unknown", - mimeType: metadata.mimeType || "application/octet-stream", - accessControl: metadata.access, - caption: metadata.caption, - group: metadata.group || (req.body?.group as string), - }, - // signature, - tempFilePath: upload.id, - }).catch((err: any) => { - logger.error({ err }, "Error creating tus upload record"); - }); - }); - - server.on(EVENTS.POST_RECEIVE, async (tusReq: any, upload: any) => { + server.on(EVENTS.POST_RECEIVE, async (_: any, upload: any) => { if (req.method === "PATCH" && upload) { try { - // tus server updates the offset internally before this hook fires - const offsetHeader = res.getHeader("Upload-Offset"); - const offset = offsetHeader - ? parseInt(String(offsetHeader), 10) - : upload.offset; - console.log( - "Updating offset to:", - offset, - "for upload ID:", - upload.id, - ); - - await updateTusUploadOffset(upload.id, offset); - - logger.info( - { uploadId: upload.id, offset }, - "Updated upload offset in DB", - ); + await updateTusUploadOffset(upload.id, upload.offset); } catch (err) { logger.error({ err }, "Failed to update tus upload offset"); } } }); - server.handle(req, res); + server.handle(req as any, res); }; -// Handle the base TUS endpoint -// Parse raw body for PATCH requests -// router.all("/create/tus*", (req: Request, res: Response, next: () => void) => { -// if (req.method === "PATCH") { -// // Ensure body parsing is skipped for PATCH -// console.log("Came here"); -// (req as any).bodyParsed = true; -// } -// next(); -// }); - -router.all("/create/tus*", cors(), authChain, handleTusRequest); +router.all("/create/resumable{*splat}", cors(), handleTusRequest); export default router; diff --git a/apps/api/src/tus/tus-server.ts b/apps/api/src/tus/tus-server.ts index decaa13b..1ff0e13b 100644 --- a/apps/api/src/tus/tus-server.ts +++ b/apps/api/src/tus/tus-server.ts @@ -2,226 +2,152 @@ import { Server } from "@tus/server"; import { FileStore } from "@tus/file-store"; import { tempFileDirForUploads, TUS_CHUNK_SIZE } from "../config/constants"; import logger from "../services/log"; -import { createTusUpload, updateTusUploadOffset } from "./queries"; import finalizeUpload from "./finalize"; +import * as preSignedUrlService from "../presigning/service"; +import { + NOT_ENOUGH_STORAGE, + PRESIGNED_URL_INVALID, + UNAUTHORISED, +} from "../config/strings"; +import { Apikey, User } from "@medialit/models"; +import { getApiKeyUsingKeyId } from "../apikey/queries"; +import { getUser } from "../user/queries"; +import { hasEnoughStorage } from "../media/storage-middleware"; +import { createTusUpload } from "./queries"; const store = new FileStore({ directory: `${tempFileDirForUploads}/tus-uploads`, }); export const server = new Server({ - path: "/media/create/tus", + path: "/media/create/resumable", datastore: store, - // Use absolute URLs so PATCH requests work with the same base URL - // respectForwardedHeaders: false, - // relativeLocation: false, - // async onIncomingRequest(req: any) { - // // Extract metadata from Upload-Metadata header per TUS spec - // // Format: Key1 Base64(Value1),Key2 Base64(Value2) - // const uploadMetadataHeader = req.headers["upload-metadata"] || req.headers["Upload-Metadata"]; - // console.log(uploadMetadataHeader) - // if (uploadMetadataHeader) { - // try { - // // header may be an array or a string - // const header = Array.isArray(uploadMetadataHeader) - // ? uploadMetadataHeader.join(",") - // : String(uploadMetadataHeader); - - // const metadata = header - // .split(",") - // .map((s) => s.trim()) - // .filter(Boolean) - // .reduce((acc, item) => { - // const parts = item.split(/\s+/); - // const key = parts[0]; - // const valueB64 = parts.slice(1).join(" "); - // if (!key || !valueB64) return acc; - // try { - // acc[key] = Buffer.from(valueB64, "base64").toString("utf8"); - // } catch (e) { - // // If value isn't valid base64, store raw value - // acc[key] = valueB64; - // } - // return acc; - // }, {} as Record); - - // // Store metadata in request for later use - // req.tusMetadata = metadata; - // } catch (err) { - // logger.error({ err }, "Error parsing upload metadata"); - // } - // } - // }, - // async onUploadCreate(req: any, upload: any) { - // console.log(req) - // // Get user and apikey from request (set by middleware) - // const user = req.user; - // const apikey = req.apikey; - // const signature = req.signature; - // // const metadata = req.tusMetadata || {}; - - // logger.info({ - // uploadId: upload.id, - // user, - // apikey, - // signature, - // // metadata, - // uploadSize: upload.size, - // }, "TUS onCreate event"); - - // if (!user || !apikey) { - // logger.error("Missing user or apikey in tus onCreate"); - // return; - // } - - // createTusUpload({ - // userId: user.id, - // apikey, - // uploadLength: upload.size, - // metadata: { - // // filename: metadata.filename || "unknown", - // // mimetype: metadata.mimetype || "application/octet-stream", - // // access: metadata.access, - // // caption: metadata.caption, - // // group: metadata.group || (req.body?.group as string), - // }, - // signature, - // tempFilePath: upload.id, - // }).catch((err) => { - // logger.error({ err }, "Error creating tus upload record"); - // }); - - // return { - // metadata: { - // ...upload.metadata, - // }, - // } as any; - // } -}); - -// Set up event handlers -// server.on("onRequest", async (req: any, res: any) => { -// logger.info({ -// method: req.method, -// url: req.url, -// headers: req.headers, -// }, "TUS server received request"); -// }); - -// server.on("onIncomingRequest", async (req: any) => { -// // Extract metadata from Upload-Metadata header per TUS spec -// // Format: Key1 Base64(Value1),Key2 Base64(Value2) -// const uploadMetadataHeader = req.headers["upload-metadata"] || req.headers["Upload-Metadata"]; -// if (uploadMetadataHeader) { -// try { -// // header may be an array or a string -// const header = Array.isArray(uploadMetadataHeader) -// ? uploadMetadataHeader.join(",") -// : String(uploadMetadataHeader); - -// const metadata = header -// .split(",") -// .map((s) => s.trim()) -// .filter(Boolean) -// .reduce((acc, item) => { -// const parts = item.split(/\s+/); -// const key = parts[0]; -// const valueB64 = parts.slice(1).join(" "); -// if (!key || !valueB64) return acc; -// try { -// acc[key] = Buffer.from(valueB64, "base64").toString("utf8"); -// } catch (e) { -// // If value isn't valid base64, store raw value -// acc[key] = valueB64; -// } -// return acc; -// }, {} as Record); - -// // Store metadata in request for later use -// req.tusMetadata = metadata; -// } catch (err) { -// logger.error({ err }, "Error parsing upload metadata"); -// } -// } -// }); - -// server.on("onCreate", async (req: any, upload: any) => { -// // Get user and apikey from request (set by middleware) -// const user = req.user; -// const apikey = req.apikey; -// const signature = req.signature; -// const metadata = req.tusMetadata || {}; - -// logger.info({ -// uploadId: upload.id, -// user, -// apikey, -// signature, -// metadata, -// uploadSize: upload.size, -// }, "TUS onCreate event"); - -// if (!user || !apikey) { -// logger.error("Missing user or apikey in tus onCreate"); -// return; -// } - -// createTusUpload({ -// userId: user.id, -// apikey, -// uploadLength: upload.size, -// metadata: { -// filename: metadata.filename || "unknown", -// mimetype: metadata.mimetype || "application/octet-stream", -// access: metadata.access, -// caption: metadata.caption, -// group: metadata.group || (req.body?.group as string), -// }, -// signature, -// tempFilePath: upload.id, -// }).catch((err) => { -// logger.error({ err }, "Error creating tus upload record"); -// }); -// }); + onIncomingRequest: async (req: any) => { + try { + const response = await getUserAndAPIKey(req); + if (!isUser(response)) { + throw response; + } + req.user = response.user; + req.apikey = response.apikey; + } catch (err) { + logger.error({ err }, "Error creating tus upload record"); + throw err; + } + }, + onUploadCreate: async (req: any, upload: any) => { + const metadata = upload.metadata; + const { user, apikey } = req; -server.on("onResponse", async (req: any, res: any, upload: any) => { - if (req.method === "PATCH" && upload) { try { - // tus server updates the offset internally before this hook fires - const offsetHeader = res.getHeader("Upload-Offset"); - const offset = offsetHeader - ? parseInt(String(offsetHeader), 10) - : upload.offset; + if (!(await hasEnoughStorage(upload.size, user))) { + throw { + status_code: 403, + body: NOT_ENOUGH_STORAGE, + }; + } + + await createTusUpload({ + uploadId: upload.id, + userId: user.id, + apikey: apikey!, + uploadLength: upload.size, + metadata: { + fileName: metadata.fileName || "unknown", + mimeType: metadata.mimeType || "application/octet-stream", + accessControl: metadata.access, + caption: metadata.caption, + group: metadata.group || (req.body?.group as string), + }, + tempFilePath: upload.id, + }); + } catch (err: any) { + logger.error({ err }, "Error creating tus upload record"); + throw err; + } + return metadata; + }, + onUploadFinish: async (req: any, upload: any) => { + logger.info( + { + uploadId: upload.id, + offset: upload.offset, + size: upload.size, + metadata: upload.metadata, + }, + "Option: Tus upload finished (onUploadFinish)", + ); - await updateTusUploadOffset(upload.id, offset); + try { + console.time("finalize"); + await finalizeUpload(upload.id); + console.timeEnd("finalize"); + return {}; + } catch (err: any) { + logger.error( + { err, uploadId: upload.id }, + "Error finalizing tus upload", + ); + return { + status_code: 403, + body: err.message, + }; + } + }, +}); - logger.info( - { uploadId: upload.id, offset }, - "Updated upload offset in DB", +async function getUserAndAPIKey(req: any): Promise { + const signature = req.headers.get("x-medialit-signature"); + const apikeyFromHeader = req.headers.get("x-medialit-apikey"); + let user, apikey; + if (signature) { + const response = + await preSignedUrlService.getUserAndGroupFromPresignedUrl( + signature as string, ); - } catch (err) { - logger.error({ err }, "Failed to update tus upload offset"); + if (!response) { + return { + status_code: 401, + body: PRESIGNED_URL_INVALID, + }; } + + user = response.user; + apikey = response.apikey; + } else { + const apikeyFromDB: Apikey | null = + await getApiKeyUsingKeyId(apikeyFromHeader); + if (!apikeyFromDB) { + return { + status_code: 401, + body: UNAUTHORISED, + }; + } + user = await getUser(apikeyFromDB.userId.toString()); + if (!user) { + return { + status_code: 401, + body: UNAUTHORISED, + }; + } + apikey = apikeyFromHeader; } -}); -server.on("onUploadFinish", async (req: any, upload: any) => { - logger.info( - { - uploadId: upload.id, - offset: upload.offset, - size: upload.size, - metadata: upload.metadata, - }, - "Tus upload finished (onUploadFinish)", - ); + return { user, apikey }; +} - // Finalize the upload (move to S3, generate thumbnails, etc.) - finalizeUpload(upload.id).catch((err) => { - logger.error( - { err, uploadId: upload.id }, - "Error finalizing tus upload", - ); - }); - return {}; -}); +interface UserWithAPIKey { + user: User; + apikey: string; +} + +interface TusError { + status_code: number; + body: string; +} + +function isUser( + response: UserWithAPIKey | TusError, +): response is UserWithAPIKey { + return (response as UserWithAPIKey).user?.userId !== undefined; +} diff --git a/examples/next-app-router/components/MediaList.tsx b/examples/next-app-router/components/MediaList.tsx index ef80093c..15836b9f 100644 --- a/examples/next-app-router/components/MediaList.tsx +++ b/examples/next-app-router/components/MediaList.tsx @@ -182,12 +182,42 @@ export default function MediaList() {
{selectedMedia.thumbnail ? ( - {selectedMedia.originalFileName} + selectedMedia.mimeType.startsWith( + "video", + ) ? ( + + ) : ( + {selectedMedia.originalFileName} + ) ) : ( { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 48e7f652..9dba9d98 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -87,8 +87,8 @@ importers: specifier: ^16.4.7 version: 16.4.7 express: - specifier: ^4.18.2 - version: 4.21.2 + specifier: ^5.1.0 + version: 5.1.0 express-fileupload: specifier: ^1.3.1 version: 1.5.1 @@ -2885,8 +2885,8 @@ packages: resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} deprecated: Use your platform's native atob() and btoa() methods instead - accepts@1.3.8: - resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} acorn-globals@4.3.4: @@ -3012,9 +3012,6 @@ packages: array-equal@1.0.2: resolution: {integrity: sha512-gUHx76KtnhEgB3HOuFYiCm3FIdEs6ocM2asHvNTkfu/Y09qQVrrVVaOKENmS2KkSaGoxgXNqC+ZVtR/n0MOkSA==} - array-flatten@1.1.1: - resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} - array-includes@3.1.8: resolution: {integrity: sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==} engines: {node: '>= 0.4'} @@ -3172,9 +3169,9 @@ packages: bindings@1.5.0: resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} - body-parser@1.20.3: - resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} - engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + body-parser@2.2.0: + resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} + engines: {node: '>=18'} bowser@2.11.0: resolution: {integrity: sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==} @@ -3415,8 +3412,8 @@ packages: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} - content-disposition@0.5.4: - resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + content-disposition@1.0.0: + resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==} engines: {node: '>= 0.6'} content-type@1.0.5: @@ -3429,8 +3426,9 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - cookie-signature@1.0.6: - resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} cookie@0.7.1: resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} @@ -3572,10 +3570,6 @@ packages: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} - destroy@1.2.0: - resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} - engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - detect-indent@6.1.0: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} engines: {node: '>=8'} @@ -3665,10 +3659,6 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - encodeurl@1.0.2: - resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} - engines: {node: '>= 0.8'} - encodeurl@2.0.0: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} @@ -3979,9 +3969,9 @@ packages: resolution: {integrity: sha512-LsYG1ALXEB7vlmjuSw8ABeOctMp8a31aUC5ZF55zuz7O2jLFnmJYrCv10py357ky48aEoBQ/9bVXgFynjvaPmA==} engines: {node: '>=12.0.0'} - express@4.21.2: - resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} - engines: {node: '>= 0.10.0'} + express@5.1.0: + resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} + engines: {node: '>= 18'} extend-shallow@2.0.1: resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} @@ -4067,8 +4057,8 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} - finalhandler@1.3.1: - resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} + finalhandler@2.1.0: + resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} engines: {node: '>= 0.8'} find-up@3.0.0: @@ -4128,9 +4118,9 @@ packages: resolution: {integrity: sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==} engines: {node: '>=0.10.0'} - fresh@0.5.2: - resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} - engines: {node: '>= 0.6'} + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} fs-extra@7.0.1: resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} @@ -4424,6 +4414,14 @@ packages: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + iconv-lite@0.7.0: + resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==} + engines: {node: '>=0.10.0'} + ieee754@1.1.13: resolution: {integrity: sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==} @@ -4633,6 +4631,9 @@ packages: resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} engines: {node: '>=0.10.0'} + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -5275,15 +5276,16 @@ packages: mdast-util-to-string@4.0.0: resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} - media-typer@0.3.0: - resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} - engines: {node: '>= 0.6'} + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} memory-pager@1.5.0: resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==} - merge-descriptors@1.0.3: - resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -5292,10 +5294,6 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} - methods@1.1.2: - resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} - engines: {node: '>= 0.6'} - micromark-core-commonmark@2.0.3: resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} @@ -5413,14 +5411,17 @@ packages: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + mime-types@2.1.35: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} - mime@1.6.0: - resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} - engines: {node: '>=4'} - hasBin: true + mime-types@3.0.1: + resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} + engines: {node: '>= 0.6'} mimic-fn@4.0.0: resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} @@ -5525,10 +5526,6 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - negotiator@0.6.3: - resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} - engines: {node: '>= 0.6'} - negotiator@1.0.0: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} @@ -5845,8 +5842,8 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} - path-to-regexp@0.1.12: - resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} + path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} path-type@3.0.0: resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==} @@ -6055,8 +6052,8 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - qs@6.13.0: - resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} + qs@6.14.0: + resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} engines: {node: '>=0.6'} qs@6.5.3: @@ -6084,9 +6081,9 @@ packages: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} - raw-body@2.5.2: - resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} - engines: {node: '>= 0.8'} + raw-body@3.0.1: + resolution: {integrity: sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==} + engines: {node: '>= 0.10'} react-dom@19.1.0: resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==} @@ -6349,6 +6346,10 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + rsvp@4.8.5: resolution: {integrity: sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==} engines: {node: 6.* || >= 7.*} @@ -6416,13 +6417,13 @@ packages: engines: {node: '>=10'} hasBin: true - send@0.19.0: - resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} - engines: {node: '>= 0.8.0'} + send@1.2.0: + resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} + engines: {node: '>= 18'} - serve-static@1.16.2: - resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} - engines: {node: '>= 0.8.0'} + serve-static@2.2.0: + resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} + engines: {node: '>= 18'} set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} @@ -6976,8 +6977,8 @@ packages: resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} engines: {node: '>=10'} - type-is@1.6.18: - resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} engines: {node: '>= 0.6'} typed-array-buffer@1.0.3: @@ -7114,10 +7115,6 @@ packages: util@0.12.5: resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} - utils-merge@1.0.1: - resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} - engines: {node: '>= 0.4.0'} - uuid@3.4.0: resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details. @@ -10475,10 +10472,10 @@ snapshots: abab@2.0.6: {} - accepts@1.3.8: + accepts@2.0.0: dependencies: - mime-types: 2.1.35 - negotiator: 0.6.3 + mime-types: 3.0.1 + negotiator: 1.0.0 acorn-globals@4.3.4: dependencies: @@ -10577,8 +10574,6 @@ snapshots: array-equal@1.0.2: {} - array-flatten@1.1.1: {} - array-includes@3.1.8: dependencies: call-bind: 1.0.8 @@ -10776,20 +10771,17 @@ snapshots: file-uri-to-path: 1.0.0 optional: true - body-parser@1.20.3: + body-parser@2.2.0: dependencies: bytes: 3.1.2 content-type: 1.0.5 - debug: 2.6.9 - depd: 2.0.0 - destroy: 1.2.0 + debug: 4.4.0(supports-color@5.5.0) http-errors: 2.0.0 - iconv-lite: 0.4.24 + iconv-lite: 0.6.3 on-finished: 2.4.1 - qs: 6.13.0 - raw-body: 2.5.2 - type-is: 1.6.18 - unpipe: 1.0.0 + qs: 6.14.0 + raw-body: 3.0.1 + type-is: 2.0.1 transitivePeerDependencies: - supports-color @@ -11044,7 +11036,7 @@ snapshots: consola@3.4.2: {} - content-disposition@0.5.4: + content-disposition@1.0.0: dependencies: safe-buffer: 5.2.1 @@ -11054,7 +11046,7 @@ snapshots: convert-source-map@2.0.0: {} - cookie-signature@1.0.6: {} + cookie-signature@1.2.2: {} cookie@0.7.1: {} @@ -11183,8 +11175,6 @@ snapshots: dequal@2.0.3: {} - destroy@1.2.0: {} - detect-indent@6.1.0: {} detect-libc@2.0.3: {} @@ -11261,8 +11251,6 @@ snapshots: emoji-regex@9.2.2: {} - encodeurl@1.0.2: {} - encodeurl@2.0.0: {} end-of-stream@1.4.4: @@ -11456,8 +11444,8 @@ snapshots: '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@5.8.3) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0)(eslint@8.57.1) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0)(eslint@8.57.1) + eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1) eslint-plugin-react: 7.37.5(eslint@8.57.1) eslint-plugin-react-hooks: 5.2.0(eslint@8.57.1) @@ -11476,8 +11464,8 @@ snapshots: '@typescript-eslint/parser': 5.62.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3) eslint: 9.24.0(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0)(eslint@9.24.0(jiti@2.4.2)) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0)(eslint@9.24.0(jiti@2.4.2)) + eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.24.0(jiti@2.4.2)))(eslint@9.24.0(jiti@2.4.2)) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.24.0(jiti@2.4.2)))(eslint@9.24.0(jiti@2.4.2)))(eslint@9.24.0(jiti@2.4.2)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.24.0(jiti@2.4.2)) eslint-plugin-react: 7.37.5(eslint@9.24.0(jiti@2.4.2)) eslint-plugin-react-hooks: 5.2.0(eslint@9.24.0(jiti@2.4.2)) @@ -11496,7 +11484,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0)(eslint@8.57.1): + eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.0(supports-color@5.5.0) @@ -11507,11 +11495,11 @@ snapshots: tinyglobby: 0.2.12 unrs-resolver: 1.4.1 optionalDependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0)(eslint@8.57.1) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0)(eslint@9.24.0(jiti@2.4.2)): + eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.24.0(jiti@2.4.2)))(eslint@9.24.0(jiti@2.4.2)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.0(supports-color@5.5.0) @@ -11522,33 +11510,33 @@ snapshots: tinyglobby: 0.2.12 unrs-resolver: 1.4.1 optionalDependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0)(eslint@9.24.0(jiti@2.4.2)) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.24.0(jiti@2.4.2)))(eslint@9.24.0(jiti@2.4.2)))(eslint@9.24.0(jiti@2.4.2)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0)(eslint@8.57.1): + eslint-module-utils@2.12.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@5.8.3) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0)(eslint@8.57.1) + eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@5.62.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0)(eslint@9.24.0(jiti@2.4.2)): + eslint-module-utils@2.12.0(@typescript-eslint/parser@5.62.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.24.0(jiti@2.4.2)))(eslint@9.24.0(jiti@2.4.2)))(eslint@9.24.0(jiti@2.4.2)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 5.62.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3) eslint: 9.24.0(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0)(eslint@9.24.0(jiti@2.4.2)) + eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.24.0(jiti@2.4.2)))(eslint@9.24.0(jiti@2.4.2)) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0)(eslint@8.57.1): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -11559,7 +11547,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0)(eslint@8.57.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -11577,7 +11565,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0)(eslint@9.24.0(jiti@2.4.2)): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.24.0(jiti@2.4.2)))(eslint@9.24.0(jiti@2.4.2)))(eslint@9.24.0(jiti@2.4.2)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -11588,7 +11576,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.24.0(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@5.62.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0)(eslint@9.24.0(jiti@2.4.2)) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@5.62.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@5.62.0(eslint@9.24.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.24.0(jiti@2.4.2)))(eslint@9.24.0(jiti@2.4.2)))(eslint@9.24.0(jiti@2.4.2)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -11924,38 +11912,34 @@ snapshots: dependencies: busboy: 1.6.0 - express@4.21.2: + express@5.1.0: dependencies: - accepts: 1.3.8 - array-flatten: 1.1.1 - body-parser: 1.20.3 - content-disposition: 0.5.4 + accepts: 2.0.0 + body-parser: 2.2.0 + content-disposition: 1.0.0 content-type: 1.0.5 cookie: 0.7.1 - cookie-signature: 1.0.6 - debug: 2.6.9 - depd: 2.0.0 + cookie-signature: 1.2.2 + debug: 4.4.0(supports-color@5.5.0) encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 - finalhandler: 1.3.1 - fresh: 0.5.2 + finalhandler: 2.1.0 + fresh: 2.0.0 http-errors: 2.0.0 - merge-descriptors: 1.0.3 - methods: 1.1.2 + merge-descriptors: 2.0.0 + mime-types: 3.0.1 on-finished: 2.4.1 + once: 1.4.0 parseurl: 1.3.3 - path-to-regexp: 0.1.12 proxy-addr: 2.0.7 - qs: 6.13.0 + qs: 6.14.0 range-parser: 1.2.1 - safe-buffer: 5.2.1 - send: 0.19.0 - serve-static: 1.16.2 - setprototypeof: 1.2.0 + router: 2.2.0 + send: 1.2.0 + serve-static: 2.2.0 statuses: 2.0.1 - type-is: 1.6.18 - utils-merge: 1.0.1 + type-is: 2.0.1 vary: 1.1.2 transitivePeerDependencies: - supports-color @@ -12056,15 +12040,14 @@ snapshots: dependencies: to-regex-range: 5.0.1 - finalhandler@1.3.1: + finalhandler@2.1.0: dependencies: - debug: 2.6.9 + debug: 4.4.0(supports-color@5.5.0) encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 parseurl: 1.3.3 statuses: 2.0.1 - unpipe: 1.0.0 transitivePeerDependencies: - supports-color @@ -12129,7 +12112,7 @@ snapshots: dependencies: map-cache: 0.2.2 - fresh@0.5.2: {} + fresh@2.0.0: {} fs-extra@7.0.1: dependencies: @@ -12508,6 +12491,14 @@ snapshots: dependencies: safer-buffer: 2.1.2 + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + iconv-lite@0.7.0: + dependencies: + safer-buffer: 2.1.2 + ieee754@1.1.13: {} ignore-by-default@1.0.1: {} @@ -12713,6 +12704,8 @@ snapshots: dependencies: isobject: 3.0.1 + is-promise@4.0.0: {} + is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -12964,9 +12957,7 @@ snapshots: pretty-format: 24.9.0 throat: 4.1.0 transitivePeerDependencies: - - bufferutil - supports-color - - utf-8-validate jest-leak-detector@24.9.0: dependencies: @@ -13677,18 +13668,16 @@ snapshots: dependencies: '@types/mdast': 4.0.4 - media-typer@0.3.0: {} + media-typer@1.1.0: {} memory-pager@1.5.0: {} - merge-descriptors@1.0.3: {} + merge-descriptors@2.0.0: {} merge-stream@2.0.0: {} merge2@1.4.1: {} - methods@1.1.2: {} - micromark-core-commonmark@2.0.3: dependencies: decode-named-character-reference: 1.1.0 @@ -13978,11 +13967,15 @@ snapshots: mime-db@1.52.0: {} + mime-db@1.54.0: {} + mime-types@2.1.35: dependencies: mime-db: 1.52.0 - mime@1.6.0: {} + mime-types@3.0.1: + dependencies: + mime-db: 1.54.0 mimic-fn@4.0.0: {} @@ -14084,8 +14077,6 @@ snapshots: natural-compare@1.4.0: {} - negotiator@0.6.3: {} - negotiator@1.0.0: {} next-auth@5.0.0-beta.25(next@15.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(nodemailer@6.10.0)(react@19.1.0): @@ -14411,7 +14402,7 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 - path-to-regexp@0.1.12: {} + path-to-regexp@8.3.0: {} path-type@3.0.0: dependencies: @@ -14593,7 +14584,7 @@ snapshots: punycode@2.3.1: {} - qs@6.13.0: + qs@6.14.0: dependencies: side-channel: 1.1.0 @@ -14611,11 +14602,11 @@ snapshots: range-parser@1.2.1: {} - raw-body@2.5.2: + raw-body@3.0.1: dependencies: bytes: 3.1.2 http-errors: 2.0.0 - iconv-lite: 0.4.24 + iconv-lite: 0.7.0 unpipe: 1.0.0 react-dom@19.1.0(react@19.1.0): @@ -14985,6 +14976,16 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.40.0 fsevents: 2.3.3 + router@2.2.0: + dependencies: + debug: 4.4.0(supports-color@5.5.0) + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.3.0 + transitivePeerDependencies: + - supports-color + rsvp@4.8.5: {} run-parallel@1.2.0: @@ -15055,17 +15056,15 @@ snapshots: semver@7.7.1: {} - send@0.19.0: + send@1.2.0: dependencies: - debug: 2.6.9 - depd: 2.0.0 - destroy: 1.2.0 - encodeurl: 1.0.2 + debug: 4.4.0(supports-color@5.5.0) + encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 - fresh: 0.5.2 + fresh: 2.0.0 http-errors: 2.0.0 - mime: 1.6.0 + mime-types: 3.0.1 ms: 2.1.3 on-finished: 2.4.1 range-parser: 1.2.1 @@ -15073,12 +15072,12 @@ snapshots: transitivePeerDependencies: - supports-color - serve-static@1.16.2: + serve-static@2.2.0: dependencies: encodeurl: 2.0.0 escape-html: 1.0.3 parseurl: 1.3.3 - send: 0.19.0 + send: 1.2.0 transitivePeerDependencies: - supports-color @@ -15753,10 +15752,11 @@ snapshots: type-fest@0.20.2: {} - type-is@1.6.18: + type-is@2.0.1: dependencies: - media-typer: 0.3.0 - mime-types: 2.1.35 + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.1 typed-array-buffer@1.0.3: dependencies: @@ -15966,8 +15966,6 @@ snapshots: is-typed-array: 1.1.15 which-typed-array: 1.1.19 - utils-merge@1.0.1: {} - uuid@3.4.0: {} uuid@8.0.0: {} From d0d554254eab1b21d2a5bdcf8fcb5b70b6ed3ca5 Mon Sep 17 00:00:00 2001 From: Rajat Saxena Date: Fri, 31 Oct 2025 00:03:40 +0530 Subject: [PATCH 3/8] WIP: Renamed presigned to signature; totally broken API --- apps/api/package.json | 2 +- apps/api/src/apikey/middleware.ts | 5 +- apps/api/src/config/constants.ts | 3 - apps/api/src/index.ts | 14 +- apps/api/src/media/handlers.ts | 2 +- apps/api/src/media/routes.ts | 9 +- apps/api/src/media/service.ts | 2 +- apps/api/src/presigning/routes.ts | 9 - .../src/{presigning => signature}/handlers.ts | 31 +- .../{presigning => signature}/middleware.ts | 6 +- .../src/{presigning => signature}/model.ts | 0 .../src/{presigning => signature}/queries.ts | 4 +- apps/api/src/signature/routes.ts | 8 + .../src/{presigning => signature}/service.ts | 27 +- apps/api/src/tus/cleanup.ts | 19 ++ apps/api/src/tus/finalize.ts | 7 +- apps/api/src/tus/queries.ts | 29 +- apps/api/src/tus/routes.ts | 48 +--- apps/api/src/tus/tus-server.ts | 31 +- apps/api/src/tus/utils.ts | 15 + .../next-app-router/app/api/medialit/route.ts | 9 +- .../app/api/medialit/tus/route.ts | 33 --- .../components/MediaUploadForm.tsx | 12 +- .../components/TusUploadForm.tsx | 28 +- packages/medialit/src/index.ts | 100 +++---- pnpm-lock.yaml | 270 +++++++++--------- 26 files changed, 304 insertions(+), 419 deletions(-) delete mode 100644 apps/api/src/presigning/routes.ts rename apps/api/src/{presigning => signature}/handlers.ts (52%) rename apps/api/src/{presigning => signature}/middleware.ts (80%) rename apps/api/src/{presigning => signature}/model.ts (100%) rename apps/api/src/{presigning => signature}/queries.ts (86%) create mode 100644 apps/api/src/signature/routes.ts rename apps/api/src/{presigning => signature}/service.ts (70%) create mode 100644 apps/api/src/tus/cleanup.ts create mode 100644 apps/api/src/tus/utils.ts delete mode 100644 examples/next-app-router/app/api/medialit/tus/route.ts diff --git a/apps/api/package.json b/apps/api/package.json index 5c78f281..71725386 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -43,7 +43,7 @@ "aws-sdk": "^2.1692.0", "cors": "^2.8.5", "dotenv": "^16.4.7", - "express": "^5.1.0", + "express": "^4.2.0", "express-fileupload": "^1.3.1", "joi": "^17.6.0", "mongoose": "^8.0.1", diff --git a/apps/api/src/apikey/middleware.ts b/apps/api/src/apikey/middleware.ts index 7347de33..278f8dd3 100644 --- a/apps/api/src/apikey/middleware.ts +++ b/apps/api/src/apikey/middleware.ts @@ -2,16 +2,17 @@ import { BAD_REQUEST, UNAUTHORISED } from "../config/strings"; import { getApiKeyUsingKeyId } from "./queries"; import { getUser } from "../user/queries"; import { Apikey } from "@medialit/models"; +import logger from "../services/log"; export default async function apikey( req: any, res: any, next: (...args: any[]) => void, ) { - const reqKey = req.body.apikey || req.headers["x-medialit-apikey"]; + const reqKey = req.body?.apikey || req.headers["x-medialit-apikey"]; if (!reqKey) { - console.log("API key missing in request"); + logger.error({}, "API key is missing"); return res.status(400).json({ error: BAD_REQUEST }); } diff --git a/apps/api/src/config/constants.ts b/apps/api/src/config/constants.ts index 217e5ce8..0ba5e7cd 100644 --- a/apps/api/src/config/constants.ts +++ b/apps/api/src/config/constants.ts @@ -67,6 +67,3 @@ export const HOSTNAME_OVERRIDE = process.env.HOSTNAME_OVERRIDE || ""; // Useful export const TUS_UPLOAD_EXPIRATION_HOURS = parseInt( process.env.TUS_UPLOAD_EXPIRATION_HOURS || "48", ); -export const TUS_CHUNK_SIZE = parseInt( - process.env.TUS_CHUNK_SIZE || "10485760", -); // 10MB default diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index 5417594b..8a5a8ab5 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -5,15 +5,15 @@ import express from "express"; import connectToDatabase from "./config/db"; import passport from "passport"; import mediaRoutes from "./media/routes"; -import presignedUrlRoutes from "./presigning/routes"; +import signatureRoutes from "./signature/routes"; import mediaSettingsRoutes from "./media-settings/routes"; import tusRoutes from "./tus/routes"; import logger from "./services/log"; import { createUser, findByEmail } from "./user/queries"; -import { Apikey, Constants, User } from "@medialit/models"; +import { Apikey, User } from "@medialit/models"; import { createApiKey } from "./apikey/queries"; import { spawn } from "child_process"; -import { cleanupExpiredTusUploads } from "./tus/queries"; +import { Cleanup } from "./tus/cleanup"; connectToDatabase(); const app = express(); @@ -30,7 +30,7 @@ app.get("/health", (req, res) => { }); app.use("/settings/media", mediaSettingsRoutes(passport)); -app.use("/media/presigned", presignedUrlRoutes); +app.use("/media/signature", signatureRoutes); app.use("/media", tusRoutes); app.use("/media", mediaRoutes); @@ -48,10 +48,10 @@ checkDependencies().then(() => { // Setup background cleanup job for expired tus uploads setInterval( async () => { - await cleanupExpiredTusUploads(); + await Cleanup(); }, - 1000 * 60 * 60, - ); // Run every hour + 1000 * 60 * 60, // 1 hours + ); }); async function checkDependencies() { diff --git a/apps/api/src/media/handlers.ts b/apps/api/src/media/handlers.ts index a08c1b2e..4f85584e 100644 --- a/apps/api/src/media/handlers.ts +++ b/apps/api/src/media/handlers.ts @@ -15,7 +15,7 @@ import logger from "../services/log"; import { Request } from "express"; import mediaService from "./service"; import { getMediaCount as getCount, getTotalSpace } from "./queries"; -import { Constants, getSubscriptionStatus } from "@medialit/models"; +import { getSubscriptionStatus } from "@medialit/models"; function validateUploadOptions(req: Request): Joi.ValidationResult { const uploadSchema = Joi.object({ diff --git a/apps/api/src/media/routes.ts b/apps/api/src/media/routes.ts index 32ac684d..ed6d8756 100644 --- a/apps/api/src/media/routes.ts +++ b/apps/api/src/media/routes.ts @@ -14,7 +14,7 @@ import { getMediaCount, getTotalSpaceOccupied, } from "./handlers"; -import presigned from "../presigning/middleware"; +import signatureMiddleware from "../signature/middleware"; import storage from "./storage-middleware"; const router = express.Router(); @@ -31,9 +31,12 @@ router.post( }, }), (req: Request, res: Response, next: (...args: any[]) => void) => { - const { signature } = req.query; + const signature = + req.query.signature || + req.headers["x-medialit-signature"] || + req.headers["X-Medialit-Signature"]; if (signature) { - presigned( + signatureMiddleware( req as Request & { user: any; apikey: string }, res, next, diff --git a/apps/api/src/media/service.ts b/apps/api/src/media/service.ts index df379dd1..627b54d9 100644 --- a/apps/api/src/media/service.ts +++ b/apps/api/src/media/service.ts @@ -35,7 +35,7 @@ import { getPaginatedMedia, createMedia, } from "./queries"; -import * as presignedUrlService from "../presigning/service"; +import * as presignedUrlService from "../signature/service"; import getTags from "./utils/get-tags"; import { getMainFileUrl, getThumbnailUrl } from "./utils/get-public-urls"; diff --git a/apps/api/src/presigning/routes.ts b/apps/api/src/presigning/routes.ts deleted file mode 100644 index 47ef8afd..00000000 --- a/apps/api/src/presigning/routes.ts +++ /dev/null @@ -1,9 +0,0 @@ -import express from "express"; -import apikey from "../apikey/middleware"; -import { getPresignedUrl, getPresignedTusUrl } from "./handlers"; - -const router = express.Router(); -router.post("/create", apikey, getPresignedUrl); -router.post("/tus/create", apikey, getPresignedTusUrl); - -export default router; diff --git a/apps/api/src/presigning/handlers.ts b/apps/api/src/signature/handlers.ts similarity index 52% rename from apps/api/src/presigning/handlers.ts rename to apps/api/src/signature/handlers.ts index b2d31147..a71a0b01 100644 --- a/apps/api/src/presigning/handlers.ts +++ b/apps/api/src/signature/handlers.ts @@ -12,7 +12,7 @@ function validatePresigningOptions(req: Request): Joi.ValidationResult { return uploadSchema.validate({ group }); } -export async function getPresignedUrl( +export async function getSignature( req: any, res: any, next: (...args: any[]) => void, @@ -23,40 +23,13 @@ export async function getPresignedUrl( } try { - const presignedUrl = await preSignedUrlService.generateSignedUrl({ + const signature = await preSignedUrlService.generateSignature({ userId: req.user.id, apikey: req.apikey, protocol: req.protocol, host: HOSTNAME_OVERRIDE || req.get("Host"), group: req.body.group, }); - return res.status(200).json({ message: presignedUrl }); - } catch (err: any) { - logger.error({ err }, err.message); - return res.status(500).json(err.message); - } -} - -export async function getPresignedTusUrl( - req: any, - res: any, - next: (...args: any[]) => void, -) { - const { error } = validatePresigningOptions(req); - if (error) { - return res.status(400).json({ error: error.message }); - } - - try { - const presignedUrl = await preSignedUrlService.generateSignedUrlForTus({ - userId: req.user.id, - apikey: req.apikey, - protocol: req.protocol, - host: HOSTNAME_OVERRIDE || req.get("Host"), - group: req.body.group, - }); - // Extract signature from the URL - const signature = presignedUrl.split("signature=")[1]; return res.status(200).json({ signature }); } catch (err: any) { logger.error({ err }, err.message); diff --git a/apps/api/src/presigning/middleware.ts b/apps/api/src/signature/middleware.ts similarity index 80% rename from apps/api/src/presigning/middleware.ts rename to apps/api/src/signature/middleware.ts index ab5f4825..024d00ea 100644 --- a/apps/api/src/presigning/middleware.ts +++ b/apps/api/src/signature/middleware.ts @@ -2,13 +2,15 @@ import { Request, Response } from "express"; import { PRESIGNED_URL_INVALID } from "../config/strings"; import * as preSignedUrlService from "./service"; -export default async function presigned( +export default async function signature( req: Request & { user?: any; apikey?: string }, res: Response, next: (...args: any[]) => void, ) { const signature = - req.query.signature || req.headers["x-medialit-signature"]; + req.query.signature || + req.headers["x-medialit-signature"] || + req.headers["X-Medialit-Signature"]; const response = await preSignedUrlService.getUserAndGroupFromPresignedUrl( signature as string, diff --git a/apps/api/src/presigning/model.ts b/apps/api/src/signature/model.ts similarity index 100% rename from apps/api/src/presigning/model.ts rename to apps/api/src/signature/model.ts diff --git a/apps/api/src/presigning/queries.ts b/apps/api/src/signature/queries.ts similarity index 86% rename from apps/api/src/presigning/queries.ts rename to apps/api/src/signature/queries.ts index 13094751..8fb18a26 100644 --- a/apps/api/src/presigning/queries.ts +++ b/apps/api/src/signature/queries.ts @@ -1,7 +1,5 @@ import mongoose from "mongoose"; import PreSignedUrlModel, { PreSignedUrl } from "./model"; -import { getUniqueId } from "@medialit/utils"; -import { PRESIGNED_URL_LENGTH } from "../config/constants"; export async function getPresignedUrl( signature: string, @@ -19,7 +17,7 @@ export async function createPresignedUrl( userId: string, apikey: string, group?: string, -): Promise { +): Promise { const presignedUrl = await PreSignedUrlModel.create({ userId, apikey, diff --git a/apps/api/src/signature/routes.ts b/apps/api/src/signature/routes.ts new file mode 100644 index 00000000..018c8115 --- /dev/null +++ b/apps/api/src/signature/routes.ts @@ -0,0 +1,8 @@ +import express from "express"; +import apikey from "../apikey/middleware"; +import { getSignature } from "./handlers"; + +const router = express.Router(); +router.post("/create", apikey, getSignature); + +export default router; diff --git a/apps/api/src/presigning/service.ts b/apps/api/src/signature/service.ts similarity index 70% rename from apps/api/src/presigning/service.ts rename to apps/api/src/signature/service.ts index 6cad6d79..b282716d 100644 --- a/apps/api/src/presigning/service.ts +++ b/apps/api/src/signature/service.ts @@ -42,7 +42,7 @@ interface GenerateSignedUrlProps { group?: string; } -export async function generateSignedUrl({ +export async function generateSignature({ userId, apikey, protocol, @@ -62,33 +62,10 @@ export async function generateSignedUrl({ ); }); - return `${protocol}://${host}/media/create?signature=${presignedUrl?.signature}`; + return presignedUrl.signature; } export async function cleanup(userId: string, signature: string) { await queries.deleteBySignature(signature); await queries.cleanupExpiredLinks(userId); } - -export async function generateSignedUrlForTus({ - userId, - apikey, - protocol, - host, - group, -}: GenerateSignedUrlProps): Promise { - const presignedUrl = await queries.createPresignedUrl( - userId, - apikey, - group, - ); - - queries.cleanupExpiredLinks(userId).catch((err: any) => { - logger.error( - { err }, - `Error while cleaning up expired links for ${userId}`, - ); - }); - - return `${protocol}://${host}/media/create/resumable?signature=${presignedUrl?.signature}`; -} diff --git a/apps/api/src/tus/cleanup.ts b/apps/api/src/tus/cleanup.ts new file mode 100644 index 00000000..0df5f8c6 --- /dev/null +++ b/apps/api/src/tus/cleanup.ts @@ -0,0 +1,19 @@ +import logger from "../services/log"; +import TusUploadModel, { TusUpload } from "./model"; +import { removeTusFiles } from "./utils"; + +export async function Cleanup() { + logger.info({}, "Starting the tus uploads cleanup job"); + + const now = new Date(); + const expiredUploads = (await TusUploadModel.find({ + expiresAt: { $lt: now }, + }).lean()) as unknown as TusUpload[]; + + for (const expiredUpload of expiredUploads) { + removeTusFiles(expiredUpload.tempFilePath); + await TusUploadModel.deleteOne({ _id: (expiredUpload as any)._id }); + } + + logger.info({}, "Ending the tus uploads cleanup job"); +} diff --git a/apps/api/src/tus/finalize.ts b/apps/api/src/tus/finalize.ts index 290999aa..0d178528 100644 --- a/apps/api/src/tus/finalize.ts +++ b/apps/api/src/tus/finalize.ts @@ -23,10 +23,11 @@ import generateFileName from "../media/utils/generate-file-name"; import { createMedia } from "../media/queries"; import getTags from "../media/utils/get-tags"; import { getTusUpload, markTusUploadComplete } from "./queries"; -import * as presignedUrlService from "../presigning/service"; +import * as presignedUrlService from "../signature/service"; import { getUser } from "../user/queries"; import { hasEnoughStorage } from "../media/storage-middleware"; import { NOT_ENOUGH_STORAGE } from "../config/strings"; +import { removeTusFiles } from "./utils"; export default async function finalizeUpload(uploadId: string) { logger.info({ uploadId }, "Finalizing tus upload"); @@ -80,8 +81,6 @@ export default async function finalizeUpload(uploadId: string) { const mainFilePath = `${temporaryFolderForWork}/main.${fileExtension}`; - console.log(mainFilePath, tusFilePath); - //Copy file from tus store to working directory const tusFileContent = readFileSync(tusFilePath); require("fs").writeFileSync(mainFilePath, tusFileContent); @@ -161,7 +160,7 @@ export default async function finalizeUpload(uploadId: string) { // Cleanup tus file try { if (existsSync(tusFilePath)) { - require("fs").unlinkSync(tusFilePath); + removeTusFiles(tempFilePath); } } catch (err) { logger.error({ err }, "Error cleaning up tus file"); diff --git a/apps/api/src/tus/queries.ts b/apps/api/src/tus/queries.ts index 2744e0c1..bdad43bd 100644 --- a/apps/api/src/tus/queries.ts +++ b/apps/api/src/tus/queries.ts @@ -1,3 +1,4 @@ +import { TUS_UPLOAD_EXPIRATION_HOURS } from "../config/constants"; import logger from "../services/log"; import TusUploadModel, { TusUpload } from "./model"; @@ -8,10 +9,7 @@ export async function createTusUpload( ): Promise { // const uploadId = new mongoose.Types.ObjectId().toString(); const expiresAt = new Date(); - expiresAt.setHours( - expiresAt.getHours() + - parseInt(process.env.TUS_UPLOAD_EXPIRATION_HOURS || "48"), - ); + expiresAt.setHours(expiresAt.getHours() + TUS_UPLOAD_EXPIRATION_HOURS); const tusUploadData: TusUpload = { uploadId: data.uploadId, @@ -36,13 +34,6 @@ export async function getTusUpload( return TusUploadModel.findOne({ uploadId }); } -// export async function getTusUploadBySignature( -// signature: string, -// ): Promise { -// const TusUploadModel = getTusUploadModel(); -// return TusUploadModel.findOne({ signature }); -// } - export async function updateTusUploadOffset( uploadId: string, uploadOffset: number, @@ -58,22 +49,6 @@ export async function deleteTusUpload(uploadId: string): Promise { await TusUploadModel.deleteOne({ uploadId }); } -export async function cleanupExpiredTusUploads(): Promise { - try { - const now = new Date(); - const result = await TusUploadModel.deleteMany({ - expiresAt: { $lt: now }, - }); - if (result.deletedCount > 0) { - logger.info( - `Cleaned up ${result.deletedCount} expired tus uploads`, - ); - } - } catch (err: any) { - logger.error({ err }, "Error cleaning up expired tus uploads"); - } -} - export async function getTusUploadsByUserId( userId: string, ): Promise { diff --git a/apps/api/src/tus/routes.ts b/apps/api/src/tus/routes.ts index ed1ab96f..d97bac14 100644 --- a/apps/api/src/tus/routes.ts +++ b/apps/api/src/tus/routes.ts @@ -1,53 +1,9 @@ -import express, { Request, Response } from "express"; +import express from "express"; import cors from "cors"; -import apikey from "../apikey/middleware"; -import presigned from "../presigning/middleware"; import { server } from "./tus-server"; -import logger from "../services/log"; -import { EVENTS } from "@tus/server"; -import { createTusUpload, updateTusUploadOffset } from "./queries"; -import { hasEnoughStorage } from "../media/storage-middleware"; -import { NOT_ENOUGH_STORAGE } from "../config/strings"; const router = express.Router(); -const authChain = async ( - req: Request & { user?: any; apikey?: string }, - res: Response, - next: () => void, -) => { - const signature = - req.query.signature || req.headers["x-medialit-signature"]; - - if (signature) { - presigned(req, res, next); - } else { - apikey(req, res, next); - } -}; - -const handleTusRequest = ( - req: Request & { user?: any; apikey?: string }, - res: Response, -) => { - console.log("HandleTusRequest", req.method); - if (req.method === "PATCH") { - req.setTimeout(0); // No timeout for uploads - } - - server.on(EVENTS.POST_RECEIVE, async (_: any, upload: any) => { - if (req.method === "PATCH" && upload) { - try { - await updateTusUploadOffset(upload.id, upload.offset); - } catch (err) { - logger.error({ err }, "Failed to update tus upload offset"); - } - } - }); - - server.handle(req as any, res); -}; - -router.all("/create/resumable{*splat}", cors(), handleTusRequest); +router.all("/create/resumable{*splat}", cors(), server.handle.bind(server)); export default router; diff --git a/apps/api/src/tus/tus-server.ts b/apps/api/src/tus/tus-server.ts index 1ff0e13b..ef9cc1d6 100644 --- a/apps/api/src/tus/tus-server.ts +++ b/apps/api/src/tus/tus-server.ts @@ -1,9 +1,9 @@ -import { Server } from "@tus/server"; +import { EVENTS, Server } from "@tus/server"; import { FileStore } from "@tus/file-store"; -import { tempFileDirForUploads, TUS_CHUNK_SIZE } from "../config/constants"; +import { tempFileDirForUploads } from "../config/constants"; import logger from "../services/log"; import finalizeUpload from "./finalize"; -import * as preSignedUrlService from "../presigning/service"; +import * as preSignedUrlService from "../signature/service"; import { NOT_ENOUGH_STORAGE, PRESIGNED_URL_INVALID, @@ -13,7 +13,7 @@ import { Apikey, User } from "@medialit/models"; import { getApiKeyUsingKeyId } from "../apikey/queries"; import { getUser } from "../user/queries"; import { hasEnoughStorage } from "../media/storage-middleware"; -import { createTusUpload } from "./queries"; +import { createTusUpload, updateTusUploadOffset } from "./queries"; const store = new FileStore({ directory: `${tempFileDirForUploads}/tus-uploads`, @@ -23,6 +23,7 @@ export const server = new Server({ path: "/media/create/resumable", datastore: store, onIncomingRequest: async (req: any) => { + console.log("TUS onIncomingRequest", req.method); try { const response = await getUserAndAPIKey(req); if (!isUser(response)) { @@ -31,7 +32,7 @@ export const server = new Server({ req.user = response.user; req.apikey = response.apikey; } catch (err) { - logger.error({ err }, "Error creating tus upload record"); + logger.error({ err }, "Error validating user creds"); throw err; } }, @@ -68,16 +69,6 @@ export const server = new Server({ return metadata; }, onUploadFinish: async (req: any, upload: any) => { - logger.info( - { - uploadId: upload.id, - offset: upload.offset, - size: upload.size, - metadata: upload.metadata, - }, - "Option: Tus upload finished (onUploadFinish)", - ); - try { console.time("finalize"); await finalizeUpload(upload.id); @@ -96,9 +87,16 @@ export const server = new Server({ }, }); +server.on(EVENTS.POST_RECEIVE, async (req: any, upload: any) => { + try { + await updateTusUploadOffset(upload.id, upload.offset); + } catch (err) { + logger.error({ err }, "Failed to update tus upload offset"); + } +}); + async function getUserAndAPIKey(req: any): Promise { const signature = req.headers.get("x-medialit-signature"); - const apikeyFromHeader = req.headers.get("x-medialit-apikey"); let user, apikey; if (signature) { const response = @@ -115,6 +113,7 @@ async function getUserAndAPIKey(req: any): Promise { user = response.user; apikey = response.apikey; } else { + const apikeyFromHeader = req.headers.get("x-medialit-apikey"); const apikeyFromDB: Apikey | null = await getApiKeyUsingKeyId(apikeyFromHeader); if (!apikeyFromDB) { diff --git a/apps/api/src/tus/utils.ts b/apps/api/src/tus/utils.ts new file mode 100644 index 00000000..9678d6e6 --- /dev/null +++ b/apps/api/src/tus/utils.ts @@ -0,0 +1,15 @@ +import path from "path"; +import { tempFileDirForUploads } from "../config/constants"; + +export function removeTusFiles(uploadId: string) { + const tusFilePath = path.join( + `${tempFileDirForUploads}/tus-uploads`, + uploadId, + ); + require("fs").unlinkSync(tusFilePath); + const tusJSONFilePath = path.join( + `${tempFileDirForUploads}/tus-uploads`, + `${uploadId}.json`, + ); + require("fs").unlinkSync(tusJSONFilePath); +} diff --git a/examples/next-app-router/app/api/medialit/route.ts b/examples/next-app-router/app/api/medialit/route.ts index 6f9456ef..125cf3cd 100644 --- a/examples/next-app-router/app/api/medialit/route.ts +++ b/examples/next-app-router/app/api/medialit/route.ts @@ -37,8 +37,13 @@ export async function GET(request: NextRequest) { export async function POST() { try { - const presignedUrl = await client.getPresignedUploadUrl(); - return Response.json({ presignedUrl }); + const signature = await client.getSignature(); + const sp = new URLSearchParams(); + sp.append("signature", signature); + return Response.json({ + endpoint: client.endpoint, + signature, + }); } catch (error) { if (error instanceof Error) { console.log("Error getting presigned URL:", error); diff --git a/examples/next-app-router/app/api/medialit/tus/route.ts b/examples/next-app-router/app/api/medialit/tus/route.ts deleted file mode 100644 index ff8a1db8..00000000 --- a/examples/next-app-router/app/api/medialit/tus/route.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { MediaLit } from "medialit"; - -const client = new MediaLit(); -const endpoint = process.env.MEDIALIT_ENDPOINT || "https://api.medialit.cloud"; - -export async function POST() { - try { - // Get presigned URL from SDK - it returns the full URL - const presignedUrl = await client.getPresignedUploadUrl(); - - // Extract signature from the URL (URL format: http://host/media/create?signature=xxx) - const url = new URL(presignedUrl); - const signature = url.searchParams.get("signature"); - - if (!signature) { - return Response.json( - { error: "Failed to extract signature from URL" }, - { status: 500 }, - ); - } - - return Response.json({ signature, endpoint }); - } catch (error) { - if (error instanceof Error) { - console.log("Error getting presigned signature:", error); - return Response.json({ error: error.message }, { status: 500 }); - } - return Response.json( - { error: "An unknown error occurred" }, - { status: 500 }, - ); - } -} diff --git a/examples/next-app-router/components/MediaUploadForm.tsx b/examples/next-app-router/components/MediaUploadForm.tsx index d80e3721..47959af1 100644 --- a/examples/next-app-router/components/MediaUploadForm.tsx +++ b/examples/next-app-router/components/MediaUploadForm.tsx @@ -35,10 +35,11 @@ export default function MediaUploadForm() { const presignedUrlResponse = await fetch("/api/medialit", { method: "POST", }); - const { presignedUrl, error } = await presignedUrlResponse.json(); + const { endpoint, signature, error } = + await presignedUrlResponse.json(); - if (error || !presignedUrl) { - throw new Error(error || "Failed to get presigned URL"); + if (error || !signature) { + throw new Error(error || "Failed to get signature"); } // Create FormData and append required fields @@ -48,8 +49,11 @@ export default function MediaUploadForm() { formData.append("access", isPublic ? "public" : "private"); // Upload file using presigned URL - const uploadResponse = await fetch(presignedUrl, { + const uploadResponse = await fetch(`${endpoint}/media/create`, { method: "POST", + headers: { + "x-medialit-signature": signature, + }, body: formData, }); diff --git a/examples/next-app-router/components/TusUploadForm.tsx b/examples/next-app-router/components/TusUploadForm.tsx index 019b29c7..f5ee95e2 100644 --- a/examples/next-app-router/components/TusUploadForm.tsx +++ b/examples/next-app-router/components/TusUploadForm.tsx @@ -36,20 +36,25 @@ export default function TusUploadForm() { setUploadSpeed(""); try { - // Get signature and endpoint from our API route for tus uploads - const signatureResponse = await fetch("/api/medialit/tus", { + // Get presigned URL + const presignedUrlResponse = await fetch("/api/medialit", { method: "POST", }); - const { - signature, - endpoint, - error: tusError, - } = await signatureResponse.json(); + const { endpoint, signature, error } = + await presignedUrlResponse.json(); - if (tusError || !signature) { - throw new Error( - tusError || "Failed to get signature for tus upload", - ); + if (error || !signature) { + throw new Error(error || "Failed to get signature"); + } + + // const { + // signature, + // endpoint, + // error: tusError, + // } = await signatureResponse.json(); + + if (!signature) { + throw new Error("Failed to get signature from presigned URL"); } // Use the endpoint directly since we're sending signature in headers @@ -66,6 +71,7 @@ export default function TusUploadForm() { // Create tus upload const upload = new Upload(file, { endpoint: uploadUrl, + chunkSize: 1024000, retryDelays: [0, 3000, 5000], headers: { "x-medialit-signature": signature, diff --git a/packages/medialit/src/index.ts b/packages/medialit/src/index.ts index da91d443..1ce27363 100644 --- a/packages/medialit/src/index.ts +++ b/packages/medialit/src/index.ts @@ -34,7 +34,7 @@ export type FileInput = string | Buffer | Readable; export class MediaLit { private apiKey: string; - private endpoint: string; + public endpoint: string; constructor(config?: MediaLitConfig) { this.checkBrowserEnvironment(); @@ -89,12 +89,13 @@ export class MediaLit { if (options.access) formData.append("access", options.access); if (options.caption) formData.append("caption", options.caption); if (options.group) formData.append("group", options.group); - formData.append("apikey", this.apiKey); + // formData.append("apikey", this.apiKey); const response = await fetch(`${this.endpoint}/media/create`, { method: "POST", headers: { ...formData.getHeaders(), + "x-medialit-apikey": this.apiKey, }, body: formData, }); @@ -107,33 +108,6 @@ export class MediaLit { return response.json(); } - async uploadWithPresignedUrl( - presignedUrl: string, - file: FileInput, - options: UploadOptions = {}, - ): Promise { - this.checkBrowserEnvironment(); - const { formData } = await this.createFormData(file); - - if (options.access) formData.append("access", options.access); - if (options.caption) formData.append("caption", options.caption); - if (options.group) formData.append("group", options.group); - - const response = await fetch(presignedUrl, { - method: "POST", - body: formData, - }); - - if (!response.ok) { - const error = await response.json(); - throw new Error( - error.message || "Upload with presigned URL failed", - ); - } - - return response.json(); - } - async delete(mediaId: string): Promise { this.checkBrowserEnvironment(); if (!this.apiKey) { @@ -146,10 +120,8 @@ export class MediaLit { method: "DELETE", headers: { "Content-Type": "application/json", + "x-medialit-apikey": this.apiKey, }, - body: JSON.stringify({ - apikey: this.apiKey, - }), }, ); @@ -169,10 +141,8 @@ export class MediaLit { method: "POST", headers: { "Content-Type": "application/json", + "x-medialit-apikey": this.apiKey, }, - body: JSON.stringify({ - apikey: this.apiKey, - }), }); if (!response.ok) { @@ -207,10 +177,8 @@ export class MediaLit { method: "POST", headers: { "Content-Type": "application/json", + "x-medialit-apikey": this.apiKey, }, - body: JSON.stringify({ - apikey: this.apiKey, - }), }, ); @@ -222,23 +190,21 @@ export class MediaLit { return response.json(); } - async getPresignedUploadUrl( - options: { group?: string } = {}, - ): Promise { + async getSignature(options: { group?: string } = {}): Promise { this.checkBrowserEnvironment(); if (!this.apiKey) { throw new Error(API_KEY_REQUIRED); } const response = await fetch( - `${this.endpoint}/media/presigned/create`, + `${this.endpoint}/media/signature/create`, { method: "POST", headers: { "Content-Type": "application/json", + "x-medialit-apikey": this.apiKey, }, body: JSON.stringify({ - apikey: this.apiKey, ...(options.group ? { group: options.group } : {}), }), }, @@ -246,11 +212,11 @@ export class MediaLit { if (!response.ok) { const error = await response.json(); - throw new Error(error.message || "Failed to get presigned URL"); + throw new Error(error.message || "Failed to get signature"); } const result = await response.json(); - return result.message; + return result.signature; } async getCount(): Promise { @@ -263,10 +229,8 @@ export class MediaLit { method: "POST", headers: { "Content-Type": "application/json", + "x-medialit-apikey": this.apiKey, }, - body: JSON.stringify({ - apikey: this.apiKey, - }), }); if (!response.ok) { @@ -288,10 +252,8 @@ export class MediaLit { method: "POST", headers: { "Content-Type": "application/json", + "x-medialit-apikey": this.apiKey, }, - body: JSON.stringify({ - apikey: this.apiKey, - }), }); if (!response.ok) { @@ -312,10 +274,8 @@ export class MediaLit { method: "POST", headers: { "Content-Type": "application/json", + "x-medialit-apikey": this.apiKey, }, - body: JSON.stringify({ - apikey: this.apiKey, - }), }); if (!response.ok) { @@ -336,9 +296,9 @@ export class MediaLit { method: "POST", headers: { "Content-Type": "application/json", + "x-medialit-apikey": this.apiKey, }, body: JSON.stringify({ - apikey: this.apiKey, ...settings, }), }); @@ -348,4 +308,34 @@ export class MediaLit { throw new Error(error.message || "Failed to update media settings"); } } + + // async signedUpload( + // signature: string, + // file: FileInput, + // options: UploadOptions = {}, + // ): Promise { + // this.checkBrowserEnvironment(); + // const { formData } = await this.createFormData(file); + + // if (options.access) formData.append("access", options.access); + // if (options.caption) formData.append("caption", options.caption); + // if (options.group) formData.append("group", options.group); + + // const response = await fetch(this.endpoint, { + // method: "POST", + // body: formData, + // headers: { + // "x-medialit-signature": signature + // } + // }); + + // if (!response.ok) { + // const error = await response.json(); + // throw new Error( + // error.message || "Upload with signature failed", + // ); + // } + + // return response.json(); + // } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9dba9d98..793cb0d2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -87,8 +87,8 @@ importers: specifier: ^16.4.7 version: 16.4.7 express: - specifier: ^5.1.0 - version: 5.1.0 + specifier: ^4.2.0 + version: 4.21.2 express-fileupload: specifier: ^1.3.1 version: 1.5.1 @@ -2885,8 +2885,8 @@ packages: resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} deprecated: Use your platform's native atob() and btoa() methods instead - accepts@2.0.0: - resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} acorn-globals@4.3.4: @@ -3012,6 +3012,9 @@ packages: array-equal@1.0.2: resolution: {integrity: sha512-gUHx76KtnhEgB3HOuFYiCm3FIdEs6ocM2asHvNTkfu/Y09qQVrrVVaOKENmS2KkSaGoxgXNqC+ZVtR/n0MOkSA==} + array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + array-includes@3.1.8: resolution: {integrity: sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==} engines: {node: '>= 0.4'} @@ -3169,9 +3172,9 @@ packages: bindings@1.5.0: resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} - body-parser@2.2.0: - resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} - engines: {node: '>=18'} + body-parser@1.20.3: + resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} bowser@2.11.0: resolution: {integrity: sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==} @@ -3412,8 +3415,8 @@ packages: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} - content-disposition@1.0.0: - resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==} + content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} content-type@1.0.5: @@ -3426,9 +3429,8 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - cookie-signature@1.2.2: - resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} - engines: {node: '>=6.6.0'} + cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} cookie@0.7.1: resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} @@ -3570,6 +3572,10 @@ packages: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + detect-indent@6.1.0: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} engines: {node: '>=8'} @@ -3659,6 +3665,10 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + encodeurl@2.0.0: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} @@ -3969,9 +3979,9 @@ packages: resolution: {integrity: sha512-LsYG1ALXEB7vlmjuSw8ABeOctMp8a31aUC5ZF55zuz7O2jLFnmJYrCv10py357ky48aEoBQ/9bVXgFynjvaPmA==} engines: {node: '>=12.0.0'} - express@5.1.0: - resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} - engines: {node: '>= 18'} + express@4.21.2: + resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} + engines: {node: '>= 0.10.0'} extend-shallow@2.0.1: resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} @@ -4057,8 +4067,8 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} - finalhandler@2.1.0: - resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} + finalhandler@1.3.1: + resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} engines: {node: '>= 0.8'} find-up@3.0.0: @@ -4118,9 +4128,9 @@ packages: resolution: {integrity: sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==} engines: {node: '>=0.10.0'} - fresh@2.0.0: - resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} - engines: {node: '>= 0.8'} + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} fs-extra@7.0.1: resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} @@ -4414,14 +4424,6 @@ packages: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} - iconv-lite@0.6.3: - resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} - engines: {node: '>=0.10.0'} - - iconv-lite@0.7.0: - resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==} - engines: {node: '>=0.10.0'} - ieee754@1.1.13: resolution: {integrity: sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==} @@ -4631,9 +4633,6 @@ packages: resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} engines: {node: '>=0.10.0'} - is-promise@4.0.0: - resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} - is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -5276,16 +5275,15 @@ packages: mdast-util-to-string@4.0.0: resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} - media-typer@1.1.0: - resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} - engines: {node: '>= 0.8'} + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} memory-pager@1.5.0: resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==} - merge-descriptors@2.0.0: - resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} - engines: {node: '>=18'} + merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -5294,6 +5292,10 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + micromark-core-commonmark@2.0.3: resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} @@ -5411,17 +5413,14 @@ packages: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} - mime-db@1.54.0: - resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} - engines: {node: '>= 0.6'} - mime-types@2.1.35: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} - mime-types@3.0.1: - resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} - engines: {node: '>= 0.6'} + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true mimic-fn@4.0.0: resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} @@ -5526,6 +5525,10 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + negotiator@1.0.0: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} @@ -5842,8 +5845,8 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} - path-to-regexp@8.3.0: - resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + path-to-regexp@0.1.12: + resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} path-type@3.0.0: resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==} @@ -6052,8 +6055,8 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - qs@6.14.0: - resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + qs@6.13.0: + resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} engines: {node: '>=0.6'} qs@6.5.3: @@ -6081,9 +6084,9 @@ packages: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} - raw-body@3.0.1: - resolution: {integrity: sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==} - engines: {node: '>= 0.10'} + raw-body@2.5.2: + resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} + engines: {node: '>= 0.8'} react-dom@19.1.0: resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==} @@ -6346,10 +6349,6 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true - router@2.2.0: - resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} - engines: {node: '>= 18'} - rsvp@4.8.5: resolution: {integrity: sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==} engines: {node: 6.* || >= 7.*} @@ -6417,13 +6416,13 @@ packages: engines: {node: '>=10'} hasBin: true - send@1.2.0: - resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} - engines: {node: '>= 18'} + send@0.19.0: + resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} + engines: {node: '>= 0.8.0'} - serve-static@2.2.0: - resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} - engines: {node: '>= 18'} + serve-static@1.16.2: + resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} + engines: {node: '>= 0.8.0'} set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} @@ -6977,8 +6976,8 @@ packages: resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} engines: {node: '>=10'} - type-is@2.0.1: - resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} typed-array-buffer@1.0.3: @@ -7115,6 +7114,10 @@ packages: util@0.12.5: resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + uuid@3.4.0: resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details. @@ -10472,10 +10475,10 @@ snapshots: abab@2.0.6: {} - accepts@2.0.0: + accepts@1.3.8: dependencies: - mime-types: 3.0.1 - negotiator: 1.0.0 + mime-types: 2.1.35 + negotiator: 0.6.3 acorn-globals@4.3.4: dependencies: @@ -10574,6 +10577,8 @@ snapshots: array-equal@1.0.2: {} + array-flatten@1.1.1: {} + array-includes@3.1.8: dependencies: call-bind: 1.0.8 @@ -10771,17 +10776,20 @@ snapshots: file-uri-to-path: 1.0.0 optional: true - body-parser@2.2.0: + body-parser@1.20.3: dependencies: bytes: 3.1.2 content-type: 1.0.5 - debug: 4.4.0(supports-color@5.5.0) + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 http-errors: 2.0.0 - iconv-lite: 0.6.3 + iconv-lite: 0.4.24 on-finished: 2.4.1 - qs: 6.14.0 - raw-body: 3.0.1 - type-is: 2.0.1 + qs: 6.13.0 + raw-body: 2.5.2 + type-is: 1.6.18 + unpipe: 1.0.0 transitivePeerDependencies: - supports-color @@ -11036,7 +11044,7 @@ snapshots: consola@3.4.2: {} - content-disposition@1.0.0: + content-disposition@0.5.4: dependencies: safe-buffer: 5.2.1 @@ -11046,7 +11054,7 @@ snapshots: convert-source-map@2.0.0: {} - cookie-signature@1.2.2: {} + cookie-signature@1.0.6: {} cookie@0.7.1: {} @@ -11175,6 +11183,8 @@ snapshots: dequal@2.0.3: {} + destroy@1.2.0: {} + detect-indent@6.1.0: {} detect-libc@2.0.3: {} @@ -11251,6 +11261,8 @@ snapshots: emoji-regex@9.2.2: {} + encodeurl@1.0.2: {} + encodeurl@2.0.0: {} end-of-stream@1.4.4: @@ -11912,34 +11924,38 @@ snapshots: dependencies: busboy: 1.6.0 - express@5.1.0: + express@4.21.2: dependencies: - accepts: 2.0.0 - body-parser: 2.2.0 - content-disposition: 1.0.0 + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.3 + content-disposition: 0.5.4 content-type: 1.0.5 cookie: 0.7.1 - cookie-signature: 1.2.2 - debug: 4.4.0(supports-color@5.5.0) + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 - finalhandler: 2.1.0 - fresh: 2.0.0 + finalhandler: 1.3.1 + fresh: 0.5.2 http-errors: 2.0.0 - merge-descriptors: 2.0.0 - mime-types: 3.0.1 + merge-descriptors: 1.0.3 + methods: 1.1.2 on-finished: 2.4.1 - once: 1.4.0 parseurl: 1.3.3 + path-to-regexp: 0.1.12 proxy-addr: 2.0.7 - qs: 6.14.0 + qs: 6.13.0 range-parser: 1.2.1 - router: 2.2.0 - send: 1.2.0 - serve-static: 2.2.0 + safe-buffer: 5.2.1 + send: 0.19.0 + serve-static: 1.16.2 + setprototypeof: 1.2.0 statuses: 2.0.1 - type-is: 2.0.1 + type-is: 1.6.18 + utils-merge: 1.0.1 vary: 1.1.2 transitivePeerDependencies: - supports-color @@ -12040,14 +12056,15 @@ snapshots: dependencies: to-regex-range: 5.0.1 - finalhandler@2.1.0: + finalhandler@1.3.1: dependencies: - debug: 4.4.0(supports-color@5.5.0) + debug: 2.6.9 encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 parseurl: 1.3.3 statuses: 2.0.1 + unpipe: 1.0.0 transitivePeerDependencies: - supports-color @@ -12112,7 +12129,7 @@ snapshots: dependencies: map-cache: 0.2.2 - fresh@2.0.0: {} + fresh@0.5.2: {} fs-extra@7.0.1: dependencies: @@ -12491,14 +12508,6 @@ snapshots: dependencies: safer-buffer: 2.1.2 - iconv-lite@0.6.3: - dependencies: - safer-buffer: 2.1.2 - - iconv-lite@0.7.0: - dependencies: - safer-buffer: 2.1.2 - ieee754@1.1.13: {} ignore-by-default@1.0.1: {} @@ -12704,8 +12713,6 @@ snapshots: dependencies: isobject: 3.0.1 - is-promise@4.0.0: {} - is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -13668,16 +13675,18 @@ snapshots: dependencies: '@types/mdast': 4.0.4 - media-typer@1.1.0: {} + media-typer@0.3.0: {} memory-pager@1.5.0: {} - merge-descriptors@2.0.0: {} + merge-descriptors@1.0.3: {} merge-stream@2.0.0: {} merge2@1.4.1: {} + methods@1.1.2: {} + micromark-core-commonmark@2.0.3: dependencies: decode-named-character-reference: 1.1.0 @@ -13967,15 +13976,11 @@ snapshots: mime-db@1.52.0: {} - mime-db@1.54.0: {} - mime-types@2.1.35: dependencies: mime-db: 1.52.0 - mime-types@3.0.1: - dependencies: - mime-db: 1.54.0 + mime@1.6.0: {} mimic-fn@4.0.0: {} @@ -14077,6 +14082,8 @@ snapshots: natural-compare@1.4.0: {} + negotiator@0.6.3: {} + negotiator@1.0.0: {} next-auth@5.0.0-beta.25(next@15.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(nodemailer@6.10.0)(react@19.1.0): @@ -14402,7 +14409,7 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 - path-to-regexp@8.3.0: {} + path-to-regexp@0.1.12: {} path-type@3.0.0: dependencies: @@ -14584,7 +14591,7 @@ snapshots: punycode@2.3.1: {} - qs@6.14.0: + qs@6.13.0: dependencies: side-channel: 1.1.0 @@ -14602,11 +14609,11 @@ snapshots: range-parser@1.2.1: {} - raw-body@3.0.1: + raw-body@2.5.2: dependencies: bytes: 3.1.2 http-errors: 2.0.0 - iconv-lite: 0.7.0 + iconv-lite: 0.4.24 unpipe: 1.0.0 react-dom@19.1.0(react@19.1.0): @@ -14976,16 +14983,6 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.40.0 fsevents: 2.3.3 - router@2.2.0: - dependencies: - debug: 4.4.0(supports-color@5.5.0) - depd: 2.0.0 - is-promise: 4.0.0 - parseurl: 1.3.3 - path-to-regexp: 8.3.0 - transitivePeerDependencies: - - supports-color - rsvp@4.8.5: {} run-parallel@1.2.0: @@ -15056,15 +15053,17 @@ snapshots: semver@7.7.1: {} - send@1.2.0: + send@0.19.0: dependencies: - debug: 4.4.0(supports-color@5.5.0) - encodeurl: 2.0.0 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 escape-html: 1.0.3 etag: 1.8.1 - fresh: 2.0.0 + fresh: 0.5.2 http-errors: 2.0.0 - mime-types: 3.0.1 + mime: 1.6.0 ms: 2.1.3 on-finished: 2.4.1 range-parser: 1.2.1 @@ -15072,12 +15071,12 @@ snapshots: transitivePeerDependencies: - supports-color - serve-static@2.2.0: + serve-static@1.16.2: dependencies: encodeurl: 2.0.0 escape-html: 1.0.3 parseurl: 1.3.3 - send: 1.2.0 + send: 0.19.0 transitivePeerDependencies: - supports-color @@ -15752,11 +15751,10 @@ snapshots: type-fest@0.20.2: {} - type-is@2.0.1: + type-is@1.6.18: dependencies: - content-type: 1.0.5 - media-typer: 1.1.0 - mime-types: 3.0.1 + media-typer: 0.3.0 + mime-types: 2.1.35 typed-array-buffer@1.0.3: dependencies: @@ -15966,6 +15964,8 @@ snapshots: is-typed-array: 1.1.15 which-typed-array: 1.1.19 + utils-merge@1.0.1: {} + uuid@3.4.0: {} uuid@8.0.0: {} From d60fc1b63c67d9549c621ad16ebcacbcff1b7571 Mon Sep 17 00:00:00 2001 From: Rajat Saxena Date: Fri, 31 Oct 2025 09:12:53 +0530 Subject: [PATCH 4/8] cors fixed; signature extraction fixed; --- apps/api/src/media/handlers.ts | 3 ++- apps/api/src/media/routes.ts | 7 +++---- apps/api/src/signature/middleware.ts | 6 ++---- apps/api/src/signature/utils.ts | 7 +++++++ apps/api/src/tus/routes.ts | 2 +- 5 files changed, 15 insertions(+), 10 deletions(-) create mode 100644 apps/api/src/signature/utils.ts diff --git a/apps/api/src/media/handlers.ts b/apps/api/src/media/handlers.ts index 4f85584e..b76421bb 100644 --- a/apps/api/src/media/handlers.ts +++ b/apps/api/src/media/handlers.ts @@ -16,6 +16,7 @@ import { Request } from "express"; import mediaService from "./service"; import { getMediaCount as getCount, getTotalSpace } from "./queries"; import { getSubscriptionStatus } from "@medialit/models"; +import { getSignatureFromReq } from "../signature/utils"; function validateUploadOptions(req: Request): Joi.ValidationResult { const uploadSchema = Joi.object({ @@ -69,7 +70,7 @@ export async function uploadMedia( access, caption, group, - signature: req.query.signature, + signature: getSignatureFromReq(req), }); const media = await mediaService.getMediaDetails({ diff --git a/apps/api/src/media/routes.ts b/apps/api/src/media/routes.ts index ed6d8756..f26f957c 100644 --- a/apps/api/src/media/routes.ts +++ b/apps/api/src/media/routes.ts @@ -16,9 +16,11 @@ import { } from "./handlers"; import signatureMiddleware from "../signature/middleware"; import storage from "./storage-middleware"; +import { getSignatureFromReq } from "../signature/utils"; const router = express.Router(); +router.options("/create", cors()); router.post( "/create", cors(), @@ -31,10 +33,7 @@ router.post( }, }), (req: Request, res: Response, next: (...args: any[]) => void) => { - const signature = - req.query.signature || - req.headers["x-medialit-signature"] || - req.headers["X-Medialit-Signature"]; + const signature = getSignatureFromReq(req); if (signature) { signatureMiddleware( req as Request & { user: any; apikey: string }, diff --git a/apps/api/src/signature/middleware.ts b/apps/api/src/signature/middleware.ts index 024d00ea..2cd1550e 100644 --- a/apps/api/src/signature/middleware.ts +++ b/apps/api/src/signature/middleware.ts @@ -1,16 +1,14 @@ import { Request, Response } from "express"; import { PRESIGNED_URL_INVALID } from "../config/strings"; import * as preSignedUrlService from "./service"; +import { getSignatureFromReq } from "./utils"; export default async function signature( req: Request & { user?: any; apikey?: string }, res: Response, next: (...args: any[]) => void, ) { - const signature = - req.query.signature || - req.headers["x-medialit-signature"] || - req.headers["X-Medialit-Signature"]; + const signature = getSignatureFromReq(req); const response = await preSignedUrlService.getUserAndGroupFromPresignedUrl( signature as string, diff --git a/apps/api/src/signature/utils.ts b/apps/api/src/signature/utils.ts new file mode 100644 index 00000000..abaf2ed7 --- /dev/null +++ b/apps/api/src/signature/utils.ts @@ -0,0 +1,7 @@ +export function getSignatureFromReq(req: any) { + return ( + req.query.signature || + req.headers["x-medialit-signature"] || + req.headers["X-Medialit-Signature"] + ); +} diff --git a/apps/api/src/tus/routes.ts b/apps/api/src/tus/routes.ts index d97bac14..59d3d632 100644 --- a/apps/api/src/tus/routes.ts +++ b/apps/api/src/tus/routes.ts @@ -4,6 +4,6 @@ import { server } from "./tus-server"; const router = express.Router(); -router.all("/create/resumable{*splat}", cors(), server.handle.bind(server)); +router.all("/create/resumable*", cors(), server.handle.bind(server)); export default router; From 5dd4107b716469ebcec1f1ab9962e1865d07187e Mon Sep 17 00:00:00 2001 From: Rajat Saxena Date: Fri, 31 Oct 2025 12:13:29 +0530 Subject: [PATCH 5/8] Tests fixes --- apps/api/__tests__/media/handlers.test.ts | 2 ++ .../__tests__/media/storage-middleware.test.ts | 15 +++++---------- apps/api/src/tus/finalize.ts | 7 ------- apps/api/src/tus/tus-server.ts | 2 +- 4 files changed, 8 insertions(+), 18 deletions(-) diff --git a/apps/api/__tests__/media/handlers.test.ts b/apps/api/__tests__/media/handlers.test.ts index a0c1e9d1..3076d217 100644 --- a/apps/api/__tests__/media/handlers.test.ts +++ b/apps/api/__tests__/media/handlers.test.ts @@ -80,6 +80,7 @@ describe("Media handlers", () => { }, body: {}, query: {}, + headers: {}, }; const res = { @@ -97,6 +98,7 @@ describe("Media handlers", () => { ); const response = await uploadMedia(req, res, () => {}); + console.log("Response", response); assert.strictEqual(response.code, 200); }); }); diff --git a/apps/api/__tests__/media/storage-middleware.test.ts b/apps/api/__tests__/media/storage-middleware.test.ts index fb87306f..27277eb1 100644 --- a/apps/api/__tests__/media/storage-middleware.test.ts +++ b/apps/api/__tests__/media/storage-middleware.test.ts @@ -7,6 +7,7 @@ import { maxStorageAllowedNotSubscribed, maxStorageAllowedSubscribed, } from "../../src/config/constants"; +import { NOT_ENOUGH_STORAGE } from "../../src/config/strings"; describe("storageValidation middleware", () => { afterEach(() => { @@ -108,11 +109,8 @@ describe("storageValidation middleware", () => { }; const response = await storageValidation(req, res, next); - assert.strictEqual(response.code, 400); - assert.strictEqual( - response.data.error, - "You do not have enough storage space in your account to upload this file", - ); + assert.strictEqual(response.code, 403); + assert.strictEqual(response.data.error, NOT_ENOUGH_STORAGE); assert.strictEqual(nextCalled, false); }); @@ -145,11 +143,8 @@ describe("storageValidation middleware", () => { }; const response = await storageValidation(req, res, next); - assert.strictEqual(response.code, 400); - assert.strictEqual( - response.data.error, - "You do not have enough storage space in your account to upload this file", - ); + assert.strictEqual(response.code, 403); + assert.strictEqual(response.data.error, NOT_ENOUGH_STORAGE); assert.strictEqual(nextCalled, false); }); diff --git a/apps/api/src/tus/finalize.ts b/apps/api/src/tus/finalize.ts index 0d178528..8c5d1932 100644 --- a/apps/api/src/tus/finalize.ts +++ b/apps/api/src/tus/finalize.ts @@ -30,8 +30,6 @@ import { NOT_ENOUGH_STORAGE } from "../config/strings"; import { removeTusFiles } from "./utils"; export default async function finalizeUpload(uploadId: string) { - logger.info({ uploadId }, "Finalizing tus upload"); - const tusUpload = await getTusUpload(uploadId); if (!tusUpload) { throw new Error(`Tus upload not found: ${uploadId}`); @@ -166,11 +164,6 @@ export default async function finalizeUpload(uploadId: string) { logger.error({ err }, "Error cleaning up tus file"); } - logger.info( - { uploadId, mediaId: media.mediaId }, - "Tus upload finalized successfully", - ); - return media.mediaId; } diff --git a/apps/api/src/tus/tus-server.ts b/apps/api/src/tus/tus-server.ts index ef9cc1d6..ba022bbe 100644 --- a/apps/api/src/tus/tus-server.ts +++ b/apps/api/src/tus/tus-server.ts @@ -22,8 +22,8 @@ const store = new FileStore({ export const server = new Server({ path: "/media/create/resumable", datastore: store, + respectForwardedHeaders: true, onIncomingRequest: async (req: any) => { - console.log("TUS onIncomingRequest", req.method); try { const response = await getUserAndAPIKey(req); if (!isUser(response)) { From c7d005080ef32f928fc66f512606e896316a601d Mon Sep 17 00:00:00 2001 From: Rajat Saxena Date: Fri, 31 Oct 2025 12:17:19 +0530 Subject: [PATCH 6/8] changset for medialit sdk --- .changeset/good-books-double.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/good-books-double.md diff --git a/.changeset/good-books-double.md b/.changeset/good-books-double.md new file mode 100644 index 00000000..f31e2a30 --- /dev/null +++ b/.changeset/good-books-double.md @@ -0,0 +1,5 @@ +--- +"medialit": minor +--- + +API key and signature are passed via header instead of request body From f99618f246a1a39c65397c63b33aec12ceed7173 Mon Sep 17 00:00:00 2001 From: Rajat Saxena Date: Fri, 31 Oct 2025 12:20:59 +0530 Subject: [PATCH 7/8] Added CodeQL --- .github/workflows/codeql.yml | 41 ++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..9b9cb65b --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,41 @@ +name: "CodeQL" + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + schedule: + - cron: "51 18 * * 2" + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ javascript ] + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + queries: +security-and-quality + + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: "/language:${{ matrix.language }}" From 2d3730fe8b8f6cffe9bc1003ae54124e9cf7a265 Mon Sep 17 00:00:00 2001 From: Rajat Saxena Date: Fri, 31 Oct 2025 12:39:52 +0530 Subject: [PATCH 8/8] CodeQL fixes --- apps/api/src/tus/queries.ts | 1 - examples/next-app-router/components/TusUploadForm.tsx | 10 ---------- 2 files changed, 11 deletions(-) diff --git a/apps/api/src/tus/queries.ts b/apps/api/src/tus/queries.ts index bdad43bd..8ea5ce8f 100644 --- a/apps/api/src/tus/queries.ts +++ b/apps/api/src/tus/queries.ts @@ -1,5 +1,4 @@ import { TUS_UPLOAD_EXPIRATION_HOURS } from "../config/constants"; -import logger from "../services/log"; import TusUploadModel, { TusUpload } from "./model"; type TusUploadDocument = any; diff --git a/examples/next-app-router/components/TusUploadForm.tsx b/examples/next-app-router/components/TusUploadForm.tsx index f5ee95e2..63e4dbbd 100644 --- a/examples/next-app-router/components/TusUploadForm.tsx +++ b/examples/next-app-router/components/TusUploadForm.tsx @@ -47,16 +47,6 @@ export default function TusUploadForm() { throw new Error(error || "Failed to get signature"); } - // const { - // signature, - // endpoint, - // error: tusError, - // } = await signatureResponse.json(); - - if (!signature) { - throw new Error("Failed to get signature from presigned URL"); - } - // Use the endpoint directly since we're sending signature in headers const uploadUrl = `${endpoint}/media/create/resumable`;