/** * Zod validation schemas for external data * All JSON.parse() operations on untrusted data should use these schemas (Rule 3.7) */ import { z } from 'zod'; /** * Encryption envelope format * Used for SSH keys, service credentials, module secrets * Matches EncryptedSecret interface from secrets/encryption.ts */ export const EncryptionEnvelopeSchema = z.object({ encryptedValue: z.string(), iv: z.string(), authTag: z.string(), }); export type EncryptionEnvelope = z.infer; /** * Package checksums format * Maps file paths to xxHash64 checksums */ export const PackageChecksumsSchema = z.record(z.string(), z.string()); export type PackageChecksums = z.infer; /** * Ansible inventory output from ansible-inventory --list */ export const AnsibleInventorySchema = z.object({ _meta: z.object({ hostvars: z.record(z.string(), z.unknown()), }), // Groups are dynamic, allow any additional keys all: z .object({ children: z.array(z.string()).optional(), hosts: z.array(z.string()).optional(), }) .optional(), ungrouped: z .object({ hosts: z.array(z.string()).optional(), }) .optional(), }); export type AnsibleInventory = z.infer; /** * Ansible Galaxy collection list output * Format: { "/path": { "namespace.name": { "version": "1.0.0" } } } */ export const GalaxyCollectionListSchema = z.record( z.string(), // Path z.record( z.string(), // Collection name (namespace.name) z.object({ version: z.string(), }), ), ); export type GalaxyCollectionList = z.infer; /** * Ansible Galaxy MANIFEST.json */ export const GalaxyManifestSchema = z.object({ collection_info: z .object({ namespace: z.string(), name: z.string(), version: z.string(), }) .optional(), file_manifest_file: z .object({ name: z.string(), ftype: z.string(), chksum_type: z.string(), chksum_sha256: z.string(), }) .optional(), }); export type GalaxyManifest = z.infer; /** * Ansible Galaxy FILES.json */ export const GalaxyFileEntrySchema = z.object({ name: z.string(), ftype: z.string(), chksum_type: z.string().optional(), chksum_sha256: z.string().optional(), }); export const GalaxyFilesSchema = z.object({ files: z.array(GalaxyFileEntrySchema), format: z.number().optional(), }); export type GalaxyFiles = z.infer; /** * CLI server mode request */ export const CLIServerRequestSchema = z.object({ command: z.string(), id: z.number(), }); export type CLIServerRequest = z.infer; /** * CLI server mode response (for test utilities) */ export const CLIServerResponseSchema = z.object({ exit_code: z.number(), output: z.string().optional(), error: z.string().optional(), }); export type CLIServerResponse = z.infer; /** * Helper: Parse JSON with Zod validation * Wraps JSON.parse() with schema validation and user-friendly error messages * * @param jsonString - JSON string to parse * @param schema - Zod schema to validate against * @param context - Context for error messages (e.g., "user config file", "Ansible output") * @returns Validated parsed data */ export function parseJsonWithValidation( jsonString: string, schema: z.ZodSchema, context: string, ): T { let parsed: unknown; try { parsed = JSON.parse(jsonString); } catch (error) { throw new Error( `Failed to parse ${context} as JSON: ${error instanceof Error ? error.message : 'Unknown error'}`, ); } try { return schema.parse(parsed); } catch (error) { if (error instanceof z.ZodError) { const issues = error.errors.map((e) => `${e.path.join('.')}: ${e.message}`).join(', '); throw new Error(`Invalid ${context} format: ${issues}`); } throw error; } }