import { z } from 'zod'; export const envSchema = z.object({ NODE_ENV: z.enum(['development', 'production', 'test']).default('development'), PORT: z.coerce.number().int().positive().default(3000), DATABASE_PATH: z.string().min(1).default('/data/hipctf/db.sqlite'), UPLOAD_DIR: z.string().min(1).default('/data/hipctf/uploads'), THEMES_DIR: z.string().min(1).default('./themes'), FRONTEND_DIST: z.string().min(1).default('../frontend/dist'), JWT_ACCESS_SECRET: z.string().min(32).default('dev-access-secret-change-me-please-32chars'), JWT_REFRESH_SECRET: z.string().min(32).default('dev-refresh-secret-change-me-please-32ch'), CSRF_SECRET: z.string().min(32).default('dev-csrf-secret-change-me-please-32chars!!'), JWT_ACCESS_TTL: z.string().default('15m'), JWT_REFRESH_TTL: z.string().default('7d'), ARGON2_MEMORY_COST: z.coerce.number().int().positive().default(65536), ARGON2_TIME_COST: z.coerce.number().int().positive().default(3), ARGON2_PARALLELISM: z.coerce.number().int().positive().default(1), PASSWORD_MIN_LENGTH: z.coerce.number().int().positive().default(12), PASSWORD_REQUIRE_MIXED: z.coerce.boolean().default(true), CORS_ORIGINS: z.string().default('http://localhost:4200,http://localhost:3000'), BODY_SIZE_LIMIT: z.string().default('1mb'), UPLOAD_SIZE_LIMIT: z.string().default('50mb'), TLS_ENABLED: z.coerce.boolean().default(false), }); export type AppEnv = z.infer; export function validateEnv(raw: Record): AppEnv { const parsed = envSchema.safeParse(raw); if (!parsed.success) { const issues = parsed.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join('; '); throw new Error(`Invalid environment configuration: ${issues}`); } return parsed.data; } export const SETTINGS_KEYS = { PAGE_TITLE: 'pageTitle', LOGO: 'logo', WELCOME_MARKDOWN: 'welcomeMarkdown', THEME_KEY: 'themeKey', DEFAULT_CHALLENGE_IP: 'defaultChallengeIp', REGISTRATIONS_ENABLED: 'registrationsEnabled', EVENT_START_UTC: 'eventStartUtc', EVENT_END_UTC: 'eventEndUtc', } as const; export const DEFAULT_SETTINGS: Record = { [SETTINGS_KEYS.PAGE_TITLE]: 'HIPCTF', [SETTINGS_KEYS.LOGO]: '', [SETTINGS_KEYS.WELCOME_MARKDOWN]: '# Welcome\n\nCreate the first admin to get started.', [SETTINGS_KEYS.THEME_KEY]: 'classic', [SETTINGS_KEYS.DEFAULT_CHALLENGE_IP]: '127.0.0.1', [SETTINGS_KEYS.REGISTRATIONS_ENABLED]: 'false', [SETTINGS_KEYS.EVENT_START_UTC]: new Date(Date.now() + 60_000).toISOString(), [SETTINGS_KEYS.EVENT_END_UTC]: new Date(Date.now() + 7 * 24 * 60 * 60_000).toISOString(), }; export const SYSTEM_CATEGORY_KEYS = ['crypto', 'forensics', 'pwn', 'web', 'misc', 'osint'] as const; export type SystemCategoryKey = (typeof SYSTEM_CATEGORY_KEYS)[number]; export const SYSTEM_CATEGORY_META: Record = { crypto: { name: 'Cryptography', abbreviation: 'CRY', description: 'Crypto challenges', iconPath: '/uploads/icons/crypto.svg' }, forensics: { name: 'Forensics', abbreviation: 'FOR', description: 'Forensics challenges', iconPath: '/uploads/icons/forensics.svg' }, pwn: { name: 'Pwn', abbreviation: 'PWN', description: 'Binary exploitation', iconPath: '/uploads/icons/pwn.svg' }, web: { name: 'Web', abbreviation: 'WEB', description: 'Web exploitation', iconPath: '/uploads/icons/web.svg' }, misc: { name: 'Misc', abbreviation: 'MSC', description: 'Miscellaneous challenges', iconPath: '/uploads/icons/misc.svg' }, osint: { name: 'OSINT', abbreviation: 'OSI', description: 'Open-source intelligence', iconPath: '/uploads/icons/osint.svg' }, };