import { z } from 'zod'; export const createInstanceSchema = z.object({ name: z.string().min(2).max(100).regex(/^[a-zA-Z0-9 '_\-.]+$/, 'Name contains invalid characters'), slug: z.string().min(2).max(50).regex(/^[a-z0-9-]+$/, 'Slug must be lowercase alphanumeric with hyphens'), domain: z.string().min(3).max(255).regex(/^[a-zA-Z0-9.\-]+$/, 'Domain must be a valid hostname'), adminEmail: z.string().email(), enableMedia: z.boolean().default(false), enableChat: z.boolean().default(false), enableGancio: z.boolean().default(false), enableListmonk: z.boolean().default(false), enableMonitoring: z.boolean().default(false), enableDevTools: z.boolean().default(false), enablePayments: z.boolean().default(false), enableMeet: z.boolean().default(false), enableSms: z.boolean().default(false), enableSocial: z.boolean().default(false), enablePeople: z.boolean().default(false), enableAnalytics: z.boolean().default(false), jvbAdvertiseIp: z.string().ip({ version: 'v4' }).optional(), smtpHost: z.string().regex(/^[a-zA-Z0-9.\-]+$/, 'SMTP host must be a valid hostname').optional(), smtpPort: z.coerce.number().optional(), smtpUser: z.string().optional(), smtpFrom: z.string().optional(), emailTestMode: z.boolean().default(true), enablePangolin: z.boolean().default(false), pangolinEndpoint: z.string().url().optional(), pangolinNewtId: z.string().optional(), pangolinNewtSecret: z.string().optional(), notes: z.string().optional(), }); export const updateInstanceSchema = z.object({ name: z.string().min(2).max(100).regex(/^[a-zA-Z0-9 '_\-.]+$/, 'Name contains invalid characters').optional(), enableMedia: z.boolean().optional(), enableChat: z.boolean().optional(), enableGancio: z.boolean().optional(), enableListmonk: z.boolean().optional(), enableMonitoring: z.boolean().optional(), enableDevTools: z.boolean().optional(), enablePayments: z.boolean().optional(), enableMeet: z.boolean().optional(), enableSms: z.boolean().optional(), enableSocial: z.boolean().optional(), enablePeople: z.boolean().optional(), enableAnalytics: z.boolean().optional(), jvbAdvertiseIp: z.string().ip({ version: 'v4' }).nullable().optional(), smtpHost: z.string().regex(/^[a-zA-Z0-9.\-]+$/, 'SMTP host must be a valid hostname').optional(), smtpPort: z.coerce.number().optional(), smtpUser: z.string().optional(), smtpFrom: z.string().optional(), emailTestMode: z.boolean().optional(), notes: z.string().nullable().optional(), }); export const registerInstanceSchema = z.object({ name: z.string().min(2).max(100).regex(/^[a-zA-Z0-9 '_\-.]+$/, 'Name contains invalid characters'), slug: z.string().min(2).max(50).regex(/^[a-z0-9-]+$/, 'Slug must be lowercase alphanumeric with hyphens'), domain: z.string().min(3).max(255).regex(/^[a-zA-Z0-9.\-]+$/, 'Domain must be a valid hostname'), basePath: z.string().min(1), composeProject: z.string().min(1), portConfig: z.object({ api: z.coerce.number().int().min(1).max(65535), admin: z.coerce.number().int().min(1).max(65535), postgres: z.coerce.number().int().min(1).max(65535), nginx: z.coerce.number().int().min(1).max(65535), embed: z.coerce.number().int().min(1).max(65535).optional(), }), adminEmail: z.string().email().optional().default('admin@localhost'), enableMedia: z.boolean().default(false), enableChat: z.boolean().default(false), enableGancio: z.boolean().default(false), enableListmonk: z.boolean().default(false), enableMonitoring: z.boolean().default(false), enableDevTools: z.boolean().default(false), enablePayments: z.boolean().default(false), enableMeet: z.boolean().default(false), enableSms: z.boolean().default(false), enableSocial: z.boolean().default(false), enablePeople: z.boolean().default(false), enableAnalytics: z.boolean().default(false), emailTestMode: z.boolean().default(true), notes: z.string().optional(), }); export const reconfigureInstanceSchema = z.object({ enableMedia: z.boolean().optional(), enableChat: z.boolean().optional(), enableGancio: z.boolean().optional(), enableListmonk: z.boolean().optional(), enableMonitoring: z.boolean().optional(), enableDevTools: z.boolean().optional(), enablePayments: z.boolean().optional(), enableMeet: z.boolean().optional(), enableSms: z.boolean().optional(), enableSocial: z.boolean().optional(), enablePeople: z.boolean().optional(), enableAnalytics: z.boolean().optional(), }); export const configureTunnelSchema = z.object({ pangolinEndpoint: z.string().url(), pangolinNewtId: z.string().min(1), pangolinNewtSecret: z.string().min(1).optional(), }); export const importInstancesSchema = z.object({ instances: z.array(registerInstanceSchema).min(1).max(50), }); // SECURITY: branch name is interpolated into a shell command string in the // local `runUpgrade` path (exec, not spawn), so we must enforce the same // strict allow-list the agent uses on its own end. This blocks names starting // with `-` (avoiding flag confusion), shell metachars, and anything exotic. export const startUpgradeSchema = z.object({ skipBackup: z.boolean().optional(), useRegistry: z.boolean().optional(), branch: z .string() .regex(/^[a-zA-Z0-9][a-zA-Z0-9_.\/-]{0,99}$/, 'Invalid branch name') .optional(), }); // Approach B: image-only upgrade. Pulls images + recreates core app services // without touching tracked files. imageTag is optional — if omitted, the // agent uses whatever IMAGE_TAG the install's .env / compose env defines // (typically `latest`). Tag must be a valid Docker tag. export const startImageUpgradeSchema = z.object({ imageTag: z .string() .regex(/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}$/, 'Invalid imageTag') .optional(), }); // Approach C: release upgrade via CCP template re-render. CCP renders the // docker-compose.yml + nginx confs + pangolin resources etc. against the // tenant's context (with the proposed imageTag), writes them to the tenant, // then composePull + composeUp. Used when a release changes orchestration // in addition to image versions. imageTag is the new value for the // per-instance Instance.imageTag column (NULL falls back to env default). export const startReleaseUpgradeSchema = z.object({ imageTag: z .string() .regex(/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}$/, 'Invalid imageTag') .optional(), }); export const setupRemoteTunnelSchema = z.object({ // Empty string or omitted → resources use standard subdomains (app., api., etc.) // A value like "ck" → creates ck-app., ck-api., etc. for multi-tenant domains subdomainPrefix: z .string() .max(50) .regex(/^[a-z0-9-]*$/, 'Prefix must be lowercase alphanumeric with hyphens') .optional(), }); export type CreateInstanceInput = z.infer; export type UpdateInstanceInput = z.infer; export type RegisterInstanceInput = z.infer; export type ReconfigureInstanceInput = z.infer; export type ConfigureTunnelInput = z.infer; export type ImportInstancesInput = z.infer; export type StartUpgradeInput = z.infer;