150 lines
5.1 KiB
TypeScript
150 lines
5.1 KiB
TypeScript
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<typeof ListChallengesQuerySchema>;
|
|
|
|
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<typeof ChallengeFileManifestSchema>;
|
|
|
|
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<typeof CreateChallengeSchema>;
|
|
|
|
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<typeof UpdateChallengeSchema>;
|
|
|
|
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<typeof ImportChallengeFileSchema>;
|
|
|
|
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<typeof ImportChallengeSchema>;
|
|
|
|
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<typeof ImportDocumentSchema>;
|
|
|
|
export const StageChallengeFileResponseSchema = z.object({
|
|
token: z.string(),
|
|
originalFilename: z.string(),
|
|
mimeType: z.string(),
|
|
sizeBytes: z.number().int(),
|
|
});
|
|
export type StageChallengeFileResponse = z.infer<typeof StageChallengeFileResponseSchema>; |