import { z } from 'zod'; export const DIFFICULTIES = ['LOW', 'MEDIUM', 'HIGH'] as const; export const PROTOCOLS = ['NC', 'WEB'] as const; export const CHALLENGE_FORMAT_VERSION = 1; export const CHALLENGE_FORMAT = 'hipctf-challenges'; export const ChallengeNameSchema = z .string() .trim() .min(1, 'Name is required') .max(128, 'Name must be 128 characters or fewer'); export const ChallengeDifficultySchema = z.enum(DIFFICULTIES); export const ChallengeProtocolSchema = z.enum(PROTOCOLS); export const ChallengeIdParamSchema = z.object({ id: z.string().uuid({ message: 'Invalid challenge id' }), }); export const ListChallengesQuerySchema = z.object({ q: z.string().trim().min(1).max(128).optional(), categoryId: z.string().uuid({ message: 'Invalid category id' }).optional(), }); export type ListChallengesQuery = z.infer; export const ChallengeFileManifestSchema = z.object({ id: z.string().optional(), stagedToken: z.string().min(1).max(128).optional(), originalFilename: z.string().min(1).max(255), mimeType: z.string().min(1).max(255), sizeBytes: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER), remove: z.boolean().optional(), }); export type ChallengeFileManifest = z.infer; const portField = z .number() .int() .min(1) .max(65535) .nullable() .optional(); const baseShape = { descriptionMd: z.string().max(64_000), categoryId: z.string().uuid({ message: 'Category is required' }), difficulty: ChallengeDifficultySchema, initialPoints: z.number().int().min(1).max(100000), minimumPoints: z.number().int().min(1).max(100000), decaySolves: z.number().int().min(1).max(10000), flag: z.string().min(1, 'Flag is required').max(4096), protocol: ChallengeProtocolSchema, port: portField, ipAddress: z.string().max(255).default(''), enabled: z.boolean().default(true), files: z.array(ChallengeFileManifestSchema).max(64).optional(), }; export const CreateChallengeSchema = z .object({ name: ChallengeNameSchema, ...baseShape, }) .superRefine((val, ctx) => { if (val.minimumPoints > val.initialPoints) { ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['minimumPoints'], message: 'Minimum points must be less than or equal to initial points', }); } if (val.protocol === 'NC' && (val.port === null || val.port === undefined)) { ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['port'], message: 'Port is required for NC protocol', }); } }); export type CreateChallengePayload = z.infer; export const UpdateChallengeSchema = z .object({ name: ChallengeNameSchema.optional(), ...baseShape, files: z.array(ChallengeFileManifestSchema).max(64).optional(), }) .superRefine((val, ctx) => { if (val.initialPoints !== undefined && val.minimumPoints !== undefined && val.minimumPoints > val.initialPoints) { ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['minimumPoints'], message: 'Minimum points must be less than or equal to initial points', }); } if (val.protocol === 'NC' && (val.port === null || val.port === undefined)) { ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['port'], message: 'Port is required for NC protocol', }); } }); export type UpdateChallengePayload = z.infer; export const ImportQuerySchema = z.object({ confirm: z.string().optional(), }); export const ImportChallengeFileSchema = z.object({ originalFilename: z.string().min(1).max(255), mimeType: z.string().min(1).max(255), sizeBytes: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER), dataBase64: z.string().min(1).max(8 * 1024 * 1024), }); export type ImportChallengeFilePayload = z.infer; export const ImportChallengeSchema = z.object({ name: ChallengeNameSchema, descriptionMd: z.string().max(64_000), categorySystemKey: z.string().min(1).max(64), difficulty: ChallengeDifficultySchema, initialPoints: z.number().int().min(1).max(100000), minimumPoints: z.number().int().min(1).max(100000), decaySolves: z.number().int().min(1).max(10000), flag: z.string().min(1).max(4096), protocol: ChallengeProtocolSchema, port: portField, ipAddress: z.string().max(255).default(''), enabled: z.boolean().default(true), files: z.array(ImportChallengeFileSchema).max(64).optional(), }); export type ImportChallengePayload = z.infer; export const ImportDocumentSchema = z.object({ format: z.literal(CHALLENGE_FORMAT), version: z.number().int().min(1).max(CHALLENGE_FORMAT_VERSION), exportedAt: z.string().min(1), challenges: z.array(ImportChallengeSchema), }); export type ImportDocumentPayload = z.infer; export const StageChallengeFileResponseSchema = z.object({ token: z.string(), originalFilename: z.string(), mimeType: z.string(), sizeBytes: z.number().int(), }); export type StageChallengeFileResponse = z.infer;