Repository Public
1import { z } from "zod";23export const isoInstant = z.string().refine((s) => { try { Temporal.Instant.from(s); return true; } catch { return false; } }, "invalid instant");4export const toMs = (s: string) => Temporal.Instant.from(s).epochMilliseconds;5export const localDate = z.string().regex(/^\d{4}-\d{2}-\d{2}$/).refine((s) => { try { Temporal.PlainDate.from(s); return true; } catch { return false; } }, "invalid date");6export const localDateTime = z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/).refine((s) => { try { Temporal.PlainDateTime.from(s); return true; } catch { return false; } }, "invalid date-time");7export const timezone = z.string().max(64).refine((tz) => { try { new Intl.DateTimeFormat("en", { timeZone: tz }); return true; } catch { return false; } }, "unknown time zone");8export const locale = z.enum(["fr", "en"]);9export const email = z.string().trim().toLowerCase().max(254).pipe(z.email());10export const color = z.string().regex(/^#[0-9a-fA-F]{6}$/);11const short = (n: number) => z.string().trim().max(n);1213export const question = z.object({ key: z.string().regex(/^[a-z0-9_]{1,32}$/), label: short(120).min(1), label_en: short(120).optional().default(""), kind: z.enum(["text", "textarea"]), required: z.boolean() });1415export const eventTypeInput = z.object({16 slug: z.string().regex(/^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/),17 title: short(120).min(1), title_en: short(120).default(""),18 description: short(2000).default(""), description_en: short(2000).default(""),19 duration_min: z.number().int().min(5).max(720),20 color: color.default("#2257e0"),21 location_kind: z.enum(["video", "phone", "in_person"]),22 location_detail: short(500).default(""),23 instructions: short(2000).default(""), instructions_en: short(2000).default(""),24 languages: z.array(locale).min(1).max(2).default(["fr"]),25 approval: z.enum(["manual", "auto"]).default("manual"),26 hold_policy: z.enum(["hold", "none"]).default("hold"),27 hold_ttl_hours: z.number().int().min(1).max(720).default(48),28 buffer_before_min: z.number().int().min(0).max(240).default(0),29 buffer_after_min: z.number().int().min(0).max(240).default(0),30 slot_step_min: z.number().int().min(5).max(240).default(30),31 min_notice_min: z.number().int().min(0).max(43200).default(240),32 horizon_days: z.number().int().min(1).max(365).default(60),33 daily_limit: z.number().int().min(1).max(100).nullable().default(null),34 weekly_limit: z.number().int().min(1).max(500).nullable().default(null),35 questions: z.array(question).max(10).default([]).refine((q) => new Set(q.map((x) => x.key)).size === q.length, "duplicate question key"),36 ask_organization: z.boolean().default(true),37 require_reason: z.boolean().default(true),38 allow_visitor_cancel: z.boolean().default(true),39 allow_visitor_reschedule: z.boolean().default(true),40 reminder_hours: z.number().int().min(1).max(336).nullable().default(null),41 custom_availability: z.boolean().default(false),42 active: z.boolean().default(true),43 position: z.number().int().min(0).max(1000).default(0),44});4546export const rulesInput = z.object({47 eventTypeId: z.string().uuid().nullable().default(null),48 rules: z.array(z.object({ weekday: z.number().int().min(1).max(7), startMinute: z.number().int().min(0).max(1439), endMinute: z.number().int().min(1).max(1440) })49 .refine((r) => r.endMinute > r.startMinute, "end must be after start")).max(70),50});5152export const exceptionInput = z.discriminatedUnion("mode", [53 z.object({ mode: z.literal("range"), kind: z.enum(["open", "closed"]), eventTypeId: z.string().uuid().nullable().default(null), startsLocal: localDateTime, endsLocal: localDateTime, allDay: z.boolean().default(false), note: short(200).default("") })54 .refine((e) => e.endsLocal > e.startsLocal, "end must be after start"),55 z.object({ mode: z.literal("weekly"), kind: z.enum(["open", "closed"]), eventTypeId: z.string().uuid().nullable().default(null), weekdays: z.array(z.number().int().min(1).max(7)).min(1).max(7), startMinute: z.number().int().min(0).max(1439), durationMin: z.number().int().min(1).max(1440), validFrom: localDate, validUntil: localDate.nullable().default(null), note: short(200).default("") }),56]);5758export const settingsInput = z.object({59 timezone, locale,60 public_name: short(80).min(1), public_headline: short(160).default(""), public_headline_en: short(160).default(""),61 public_bio: short(1500).default(""), public_bio_en: short(1500).default(""),62 brand_color: color,63 notify_email: email.nullable(), reply_to: email.nullable(),64 retention_days: z.number().int().min(30).max(3650),65 allowed_email_domains: z.array(z.string().trim().toLowerCase().regex(/^[a-z0-9.-]+\.[a-z]{2,}$/)).max(50),66 blocked_email_domains: z.array(z.string().trim().toLowerCase().regex(/^[a-z0-9.-]+\.[a-z]{2,}$/)).max(200),67 external_stale_minutes: z.number().int().min(5).max(10080),68 external_stale_policy: z.enum(["conservative", "ignore"]),69});7071export const submitInput = z.object({72 slug: z.string().max(64),73 start: isoInstant,74 name: short(120).min(2),75 email,76 organization: short(160).default(""),77 reason: short(2000).default(""),78 answers: z.record(z.string().regex(/^[a-z0-9_]{1,32}$/), short(2000)).default({}),79 visitorTz: timezone,80 locale,81 idempotencyKey: z.string().uuid(),82 consent: z.literal(true),83 formToken: z.string().max(200),84 website: z.string().max(200).default(""), // honeypot: must stay empty85});86