/** * @factory/shared โ€” file DTOs (PRODUCT-SPEC ยง16). * * Uploads are two-phase: the client requests a presigned S3 PUT URL * (CreateUpload), uploads directly to storage, then confirms (CompleteUpload) * which runs the pipeline (scan โ†’ metadata โ†’ thumbnail). */ import { z } from "zod"; import { LIMITS } from "../constants.js"; export const createUploadSchema = z .object({ name: z .string() .min(1) .max(255) .refine((n) => !n.includes("..") && !n.includes("/") && !n.includes("\\"), { message: "name must be a plain file name (no paths)", }), mimeType: z .string() .min(3) .max(127) .regex(/^[a-z0-9.+-]+\/[a-z0-9.+-]+$/i, "invalid mime type"), size: z .number() .int() .min(0) .max(LIMITS.MAX_FILE_SIZE, `File exceeds the ${LIMITS.MAX_FILE_SIZE / 1024 / 1024} MB limit`), /** Block the file attaches to (image/video/audio/file blocks). */ blockId: z.string().min(1).optional(), /** Page the file belongs to (covers, attachments). */ pageId: z.string().min(1).optional(), // Both blockId and pageId are optional: a bare attachment upload is legal. }) .strict(); export type CreateUpload = z.infer; export const completeUploadSchema = z .object({ fileId: z.string().min(1), }) .strict(); export type CompleteUpload = z.infer;