import { z } from "zod"; import type { InvoiceTemplateConfig } from "./schema"; // ── Validation ───────────────────────────────────────────── // Logo data URI: must be image/* MIME with base64 payload, capped at // 500KB encoded (≈ 375KB raw image data). The cropper rasterizes to a // 400x133 PNG which is typically well under 50KB, so this gives plenty // of headroom while preventing JSON blob abuse. const MAX_LOGO_BYTES = 500 * 1024; const dataUriLogoSchema = z .string() .max(MAX_LOGO_BYTES, "Logo data URI exceeds 500KB limit") .refine( (value) => /^data:image\/[a-z+.-]+;base64,/i.test(value), "Logo must be a base64-encoded image data URI", ); // Hex color: #rgb / #rrggbb / #rrggbbaa with optional case const hexColorSchema = z .string() .regex( /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/, "Invalid hex color", ); export const invoiceTemplateConfigSchema: z.ZodType = z .object({ logo: dataUriLogoSchema.optional(), companyName: z.string().min(1).max(200), companyAddress: z.string().max(500).optional(), companyEmail: z.email().max(254).optional().or(z.literal("")), companyPhone: z.string().max(50).optional(), primaryColor: hexColorSchema, accentColor: hexColorSchema, textColor: hexColorSchema, headerStyle: z.enum(["classic", "modern", "minimal"]), showLogo: z.boolean(), showCompanyDetails: z.boolean(), showHours: z.boolean(), showRate: z.boolean(), showProjectBreakdown: z.boolean(), footerText: z.string().max(500).optional(), paymentInstructions: z.string().max(2000).optional(), paymentMethods: z .array( z.object({ type: z.string().min(1).max(50), label: z.string().min(1).max(100), details: z.record(z.string(), z.string().max(500)), }), ) .max(20) .optional(), }) // Reject unknown keys so the JSON blob can't be smuggled to // arbitrary size via fields the renderer ignores anyway. If we add // new template fields we add them here first. .strict(); /** * Validate an unknown invoice template config payload from an API * request. Throws a zod error with details on the first invalid field. */ export function parseInvoiceTemplateConfig( input: unknown, ): InvoiceTemplateConfig { return invoiceTemplateConfigSchema.parse(input); } // Default invoice template configurations export const DEFAULT_TEMPLATES: Record = { classic: { companyName: "Your Company", primaryColor: "#000000", accentColor: "#22d3ee", textColor: "#374151", headerStyle: "classic", showLogo: true, showCompanyDetails: true, showHours: true, showRate: true, showProjectBreakdown: true, footerText: "Thank you for your business!", paymentInstructions: "Payment due within 30 days.", }, modern: { companyName: "Your Company", primaryColor: "#22d3ee", accentColor: "#000000", textColor: "#1f2937", headerStyle: "modern", showLogo: true, showCompanyDetails: true, showHours: true, showRate: true, showProjectBreakdown: true, footerText: "Thank you!", }, minimal: { companyName: "Your Company", primaryColor: "#6b7280", accentColor: "#000000", textColor: "#374151", headerStyle: "minimal", showLogo: false, showCompanyDetails: false, showHours: false, showRate: false, showProjectBreakdown: false, }, }; export function getDefaultTemplate(): InvoiceTemplateConfig { return DEFAULT_TEMPLATES.classic; }