import { z } from 'zod' import { inlineFileRefQuerySchema } from '@/lib/api/contracts/primitives' import { shareRecordSchema } from '@/lib/api/contracts/public-shares' import { defineRouteContract } from '@/lib/api/contracts/types' export const workspaceFileScopeSchema = z.enum(['active', 'archived', 'all']) export const workspaceFilesParamsSchema = z.object({ id: z.string({ error: 'Workspace ID is required' }).min(1, 'Workspace ID is required'), }) export const workspaceFileParamsSchema = workspaceFilesParamsSchema.extend({ fileId: z.string({ error: 'File ID is required' }).min(1, 'File ID is required'), }) export const listWorkspaceFilesQuerySchema = z.object({ scope: workspaceFileScopeSchema.default('active'), }) /** * Binary stream of an image embedded in a workspace markdown document, scoped to the * workspace in the path. The route serves the bytes only when the referenced file is a * `workspace` file belonging to `[id]` — cross-workspace references do not resolve. */ export const getInlineWorkspaceFileContract = defineRouteContract({ method: 'GET', path: '/api/workspaces/[id]/files/inline', params: workspaceFilesParamsSchema, query: inlineFileRefQuerySchema, response: { mode: 'binary', }, }) const workspaceFileNameSchema = z .string({ error: 'Name is required' }) .trim() .min(1, 'Name is required') .refine( (name) => name !== '.' && name !== '..' && !name.includes('/') && !name.includes('\\'), 'Name cannot contain path separators or dot segments' ) export const renameWorkspaceFileBodySchema = z.object({ name: workspaceFileNameSchema, }) export const updateWorkspaceFileContentBodySchema = z.object({ content: z.string(), encoding: z.enum(['base64', 'utf-8']).optional(), }) export const workspaceFileRecordSchema = z.object({ id: z.string(), workspaceId: z.string(), name: z.string(), key: z.string(), path: z.string(), url: z.string().optional(), size: z.number(), type: z.string(), uploadedBy: z.string(), folderId: z.string().nullable(), folderPath: z.string().nullable().optional(), deletedAt: z.coerce.date().nullable().optional(), uploadedAt: z.coerce.date(), updatedAt: z.coerce.date(), storageContext: z.enum(['workspace', 'mothership']).optional(), share: shareRecordSchema.nullable().optional(), }) const workspaceFileSuccessSchema = z.object({ success: z.boolean(), }) const listWorkspaceFilesResponseSchema = workspaceFileSuccessSchema.extend({ files: z.array(workspaceFileRecordSchema), }) export type ListWorkspaceFilesResponse = z.output export const listWorkspaceFilesContract = defineRouteContract({ method: 'GET', path: '/api/workspaces/[id]/files', params: workspaceFilesParamsSchema, query: listWorkspaceFilesQuerySchema, response: { mode: 'json', schema: listWorkspaceFilesResponseSchema, }, }) export const renameWorkspaceFileContract = defineRouteContract({ method: 'PATCH', path: '/api/workspaces/[id]/files/[fileId]', params: workspaceFileParamsSchema, body: renameWorkspaceFileBodySchema, response: { mode: 'json', schema: workspaceFileSuccessSchema.extend({ file: workspaceFileRecordSchema, }), }, }) export const deleteWorkspaceFileContract = defineRouteContract({ method: 'DELETE', path: '/api/workspaces/[id]/files/[fileId]', params: workspaceFileParamsSchema, response: { mode: 'json', schema: workspaceFileSuccessSchema, }, }) export const restoreWorkspaceFileContract = defineRouteContract({ method: 'POST', path: '/api/workspaces/[id]/files/[fileId]/restore', params: workspaceFileParamsSchema, response: { mode: 'json', schema: workspaceFileSuccessSchema, }, }) export const updateWorkspaceFileContentContract = defineRouteContract({ method: 'PUT', path: '/api/workspaces/[id]/files/[fileId]/content', params: workspaceFileParamsSchema, body: updateWorkspaceFileContentBodySchema, response: { mode: 'json', schema: workspaceFileSuccessSchema.extend({ file: workspaceFileRecordSchema, }), }, }) const documentStyleSummarySchema = z .object({ format: z.enum(['docx', 'pptx', 'pdf']), // OOXML theme — present for pptx, present for docx when theme1.xml exists, absent for pdf theme: z .object({ colors: z.record(z.string(), z.string()), fonts: z.object({ major: z.string(), minor: z.string() }), }) .optional(), // docx only styles: z.array(z.object({}).passthrough()).optional(), defaults: z.object({ fontSize: z.number().optional(), font: z.string().optional() }).optional(), // pdf only pageSize: z .object({ preset: z.enum(['A4', 'letter', 'custom']), widthPt: z.number().optional(), heightPt: z.number().optional(), }) .optional(), fonts: z.array(z.string()).optional(), // pptx only slideCount: z.number().optional(), aspectRatio: z.enum(['16:9', '4:3', 'custom']).optional(), background: z.string().optional(), }) .passthrough() export const workspaceFileStyleContract = defineRouteContract({ method: 'GET', path: '/api/workspaces/[id]/files/[fileId]/style', params: workspaceFileParamsSchema, response: { mode: 'json', schema: documentStyleSummarySchema, }, }) const compiledCheckResponseSchema = z.union([ z.object({ ok: z.literal(true) }), z.object({ ok: z.literal(false), error: z.string(), errorName: z.string() }), ]) export const workspaceFileCompiledCheckContract = defineRouteContract({ method: 'GET', path: '/api/workspaces/[id]/files/[fileId]/compiled-check', params: workspaceFileParamsSchema, response: { mode: 'json', schema: compiledCheckResponseSchema, }, }) export const workspacePresignedUploadBodySchema = z.object({ fileName: workspaceFileNameSchema, contentType: z.string().min(1, 'contentType is required'), fileSize: z.number().nonnegative('fileSize must be a non-negative number'), folderId: z.string().nullable().optional(), }) export type WorkspacePresignedUploadBody = z.input const workspacePresignedFileInfoSchema = z.object({ path: z.string(), key: z.string(), name: z.string(), size: z.number(), type: z.string(), }) const workspacePresignedUploadResponseSchema = z.object({ fileName: z.string(), presignedUrl: z.string(), fileInfo: workspacePresignedFileInfoSchema, uploadHeaders: z.record(z.string(), z.string()).optional(), directUploadSupported: z.boolean(), }) export const workspacePresignedUploadContract = defineRouteContract({ method: 'POST', path: '/api/workspaces/[id]/files/presigned', params: workspaceFilesParamsSchema, body: workspacePresignedUploadBodySchema, response: { mode: 'json', schema: workspacePresignedUploadResponseSchema, }, }) export const registerWorkspaceFileBodySchema = z.object({ key: z.string().min(1, 'key is required'), name: workspaceFileNameSchema, contentType: z.string().min(1, 'contentType is required'), folderId: z.string().nullable().optional(), }) export type RegisterWorkspaceFileBody = z.input const registeredWorkspaceFileSchema = z.object({ id: z.string(), name: z.string(), url: z.string(), size: z.number(), type: z.string(), key: z.string(), context: z.string().optional(), }) const registerWorkspaceFileResponseSchema = z.object({ success: z.boolean(), file: registeredWorkspaceFileSchema.optional(), error: z.string().optional(), isDuplicate: z.boolean().optional(), }) export type RegisterWorkspaceFileResponse = z.output export const registerWorkspaceFileContract = defineRouteContract({ method: 'POST', path: '/api/workspaces/[id]/files/register', params: workspaceFilesParamsSchema, body: registerWorkspaceFileBodySchema, response: { mode: 'json', schema: registerWorkspaceFileResponseSchema, }, })