import { z } from "zod"; /** * TASK-0101/0103 — file module DTOs (zod, validated by ZodValidate). * * The upload-session body carries NO workspaceId: the workspace comes from * the `x-factory-workspace` header (the global guard's context), the same * way pages/blocks do. `replaceFileId` (TASK-0104) makes the session a new * version of an existing file instead of a fresh one. */ export const CreateUploadDto = z.object({ name: z.string().min(1).max(512), /** No upper bound here: the PLAN limit (free 50 MB / paid 500 MB, min * with the 500 MB hard ceiling) is enforced server-side with 413 * FILE_TOO_LARGE — a 400 would misreport a plan boundary as a format * error (spec TASK-0101: the limit is in the 413 body). */ size: z.number().int().positive(), mimeType: z.string().min(1).max(200), // `null` === "not attached to a page/block" — the web client always sends // these fields (defaulting to null, see lib/files/upload.ts), so the schema // must accept the explicit null, not just omit the key. `string | undefined` // input is unchanged (backward compatible: existing callers omit or send a // real id). pageId: z.string().min(1).nullable().optional(), blockId: z.string().min(1).nullable().optional(), /** TASK-0104: re-upload → new version of this existing file (accepted * here; the versioning semantics land with TASK-0104). */ replaceFileId: z.string().min(1).nullable().optional(), }); export type CreateUploadInput = z.infer; /** `PUT /files/uploads/:id/complete` — the server re-checks from S3; the * body is advisory only (kept for future checksums). */ export const CompleteUploadDto = z .object({ sha256: z.string().regex(/^[0-9a-f]{64}$/).optional(), }) .optional() .default({}); export type CompleteUploadInput = z.infer; export const CancelUploadDto = z.object({}).optional().default({}); export type CancelUploadInput = z.infer; /** `GET /files?scope=page|workspace&pageId=&kind=&status=&cursor=` (TASK-0103). */ export const ListFilesQuery = z.object({ scope: z.enum(["page", "workspace"]).default("workspace"), pageId: z.string().min(1).optional(), kind: z.enum(["image", "video", "audio", "pdf", "doc", "other"]).optional(), status: z .enum(["UPLOADING", "SCANNING", "READY", "VIRUS_DETECTED", "FAILED"]) .optional(), search: z.string().max(200).optional(), sort: z.enum(["name", "size", "date", "updated"]).default("updated"), cursor: z.string().optional(), limit: z.coerce.number().int().min(1).max(100).default(50), }); export type ListFilesInput = z.infer; export const RenameFileDto = z.object({ name: z.string().min(1).max(512) }); export type RenameFileInput = z.infer; // ── TASK-0104: versions / permissions ──────────────────────────────────────── /** `POST /files/:id/rollback` — make version n current (key swap). */ export const RollbackFileDto = z.object({ version: z.number().int().positive().max(10_000), }); export type RollbackFileInput = z.infer; /** One file-level permission grant (explicit; NONE = explicit deny). */ export const FilePermissionGrantDto = z.object({ granteeType: z.enum(["user", "team", "role"]), /** userId | teamId | role name ("OWNER"|"ADMIN"|"MEMBER"|"GUEST"). */ granteeId: z.string().min(1).max(128), /** NONE is an EXPLICIT DENY (overrides inheritance/role) — TASK-0104 §2. */ level: z.enum(["NONE", "VIEW", "COMMENT", "EDIT", "FULL_ACCESS"]), }); /** `PUT /files/:id/permissions` — REPLACE the file's explicit grants. */ export const SetPermissionsDto = z.object({ grants: z.array(FilePermissionGrantDto).max(200), }); export type SetPermissionsInput = z.infer;