/** * Notification Validation Service * * Provides runtime schema validation for notification inputs using Zod * Ensures data integrity and prevents invalid data from crossing the bridge * * @author Matthew Raymer * @version 1.0.0 */ import { z } from 'zod'; /** * Schema for basic notification options */ export const NotificationOptionsSchema = z.object({ time: z.string() .regex(/^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/, 'Invalid time format. Use HH:MM') .refine((time) => { const [hours, minutes] = time.split(':').map(Number); return hours >= 0 && hours <= 23 && minutes >= 0 && minutes <= 59; }, 'Invalid time values. Hour must be 0-23, minute must be 0-59'), title: z.string() .min(1, 'Title is required') .max(100, 'Title must be less than 100 characters') .refine((title) => title.trim().length > 0, 'Title cannot be empty'), body: z.string() .min(1, 'Body is required') .max(500, 'Body must be less than 500 characters') .refine((body) => body.trim().length > 0, 'Body cannot be empty'), sound: z.boolean().optional().default(true), priority: z.enum(['low', 'default', 'high']).optional().default('default'), url: z.string() .url('Invalid URL format') .optional() .or(z.literal('')), channel: z.string() .min(1, 'Channel is required') .max(50, 'Channel must be less than 50 characters') .optional().default('daily-notifications'), timezone: z.string() .min(1, 'Timezone is required') .refine((tz) => { try { Intl.DateTimeFormat(undefined, { timeZone: tz }); return true; } catch { return false; } }, 'Invalid timezone identifier') .optional().default('UTC') }); /** * Schema for advanced reminder options */ export const ReminderOptionsSchema = z.object({ id: z.string() .min(1, 'ID is required') .max(50, 'ID must be less than 50 characters') .regex(/^[a-zA-Z0-9_-]+$/, 'ID can only contain letters, numbers, underscores, and hyphens'), title: z.string() .min(1, 'Title is required') .max(100, 'Title must be less than 100 characters') .refine((title) => title.trim().length > 0, 'Title cannot be empty'), body: z.string() .min(1, 'Body is required') .max(500, 'Body must be less than 500 characters') .refine((body) => body.trim().length > 0, 'Body cannot be empty'), time: z.string() .regex(/^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/, 'Invalid time format. Use HH:MM') .refine((time) => { const [hours, minutes] = time.split(':').map(Number); return hours >= 0 && hours <= 23 && minutes >= 0 && minutes <= 59; }, 'Invalid time values. Hour must be 0-23, minute must be 0-59'), sound: z.boolean().optional().default(true), vibration: z.boolean().optional().default(true), priority: z.enum(['low', 'normal', 'high']).optional().default('normal'), repeatDaily: z.boolean().optional().default(true), timezone: z.string() .min(1, 'Timezone is required') .refine((tz) => { try { Intl.DateTimeFormat(undefined, { timeZone: tz }); return true; } catch { return false; } }, 'Invalid timezone identifier') .optional().default('UTC'), actions: z.array(z.object({ id: z.string().min(1).max(20), title: z.string().min(1).max(30) })).optional().default([]) }); /** * Schema for content fetch configuration */ export const ContentFetchConfigSchema = z.object({ schedule: z.string() .min(1, 'Schedule is required') .regex(/^(\*|[0-5]?\d) (\*|[0-5]?\d) (\*|[0-5]?\d) (\*|[0-5]?\d) (\*|[0-5]?\d)$/, 'Invalid cron format'), ttlSeconds: z.number() .int('TTL must be an integer') .min(60, 'TTL must be at least 60 seconds') .max(86400, 'TTL must be less than 24 hours'), source: z.string() .min(1, 'Source is required') .max(50, 'Source must be less than 50 characters'), url: z.string() .url('Invalid URL format') .min(1, 'URL is required'), headers: z.record(z.string()).optional().default({}), retryAttempts: z.number() .int('Retry attempts must be an integer') .min(0, 'Retry attempts cannot be negative') .max(10, 'Retry attempts cannot exceed 10') .optional().default(3), timeout: z.number() .int('Timeout must be an integer') .min(1000, 'Timeout must be at least 1000ms') .max(60000, 'Timeout must be less than 60 seconds') .optional().default(30000) }); /** * Schema for user notification configuration */ export const UserNotificationConfigSchema = z.object({ schedule: z.string() .min(1, 'Schedule is required') .refine((schedule) => { if (schedule === 'immediate') return true; return /^(\*|[0-5]?\d) (\*|[0-5]?\d) (\*|[0-5]?\d) (\*|[0-5]?\d) (\*|[0-5]?\d)$/.test(schedule); }, 'Invalid schedule format. Use cron format or "immediate"'), title: z.string() .min(1, 'Title is required') .max(100, 'Title must be less than 100 characters'), body: z.string() .min(1, 'Body is required') .max(500, 'Body must be less than 500 characters'), actions: z.array(z.object({ id: z.string().min(1).max(20), title: z.string().min(1).max(30) })).optional().default([]), sound: z.boolean().optional().default(true), vibration: z.boolean().optional().default(true), priority: z.enum(['low', 'normal', 'high']).optional().default('normal'), channel: z.string() .min(1, 'Channel is required') .max(50, 'Channel must be less than 50 characters') .optional().default('user-notifications') }); /** * Schema for dual schedule configuration */ export const DualScheduleConfigurationSchema = z.object({ contentFetch: ContentFetchConfigSchema, userNotification: UserNotificationConfigSchema, coordination: z.object({ enabled: z.boolean().optional().default(true), maxDelayMinutes: z.number() .int('Max delay must be an integer') .min(0, 'Max delay cannot be negative') .max(60, 'Max delay cannot exceed 60 minutes') .optional().default(10) }).optional().default({}) }); /** * Validation result interface */ export interface ValidationResult { success: boolean; data?: T; errors?: string[]; } /** * Notification Validation Service */ export class NotificationValidationService { private static instance: NotificationValidationService; private constructor() {} public static getInstance(): NotificationValidationService { if (!NotificationValidationService.instance) { NotificationValidationService.instance = new NotificationValidationService(); } return NotificationValidationService.instance; } /** * Validate basic notification options */ public validateNotificationOptions(options: unknown): ValidationResult> { try { const validatedOptions = NotificationOptionsSchema.parse(options); return { success: true, data: validatedOptions }; } catch (error) { if (error instanceof z.ZodError) { return { success: false, errors: error.errors.map(e => `${e.path.join('.')}: ${e.message}`) }; } return { success: false, errors: ['Unknown validation error'] }; } } /** * Validate reminder options */ public validateReminderOptions(options: unknown): ValidationResult> { try { const validatedOptions = ReminderOptionsSchema.parse(options); return { success: true, data: validatedOptions }; } catch (error) { if (error instanceof z.ZodError) { return { success: false, errors: error.errors.map(e => `${e.path.join('.')}: ${e.message}`) }; } return { success: false, errors: ['Unknown validation error'] }; } } /** * Validate content fetch configuration */ public validateContentFetchConfig(config: unknown): ValidationResult> { try { const validatedConfig = ContentFetchConfigSchema.parse(config); return { success: true, data: validatedConfig }; } catch (error) { if (error instanceof z.ZodError) { return { success: false, errors: error.errors.map(e => `${e.path.join('.')}: ${e.message}`) }; } return { success: false, errors: ['Unknown validation error'] }; } } /** * Validate user notification configuration */ public validateUserNotificationConfig(config: unknown): ValidationResult> { try { const validatedConfig = UserNotificationConfigSchema.parse(config); return { success: true, data: validatedConfig }; } catch (error) { if (error instanceof z.ZodError) { return { success: false, errors: error.errors.map(e => `${e.path.join('.')}: ${e.message}`) }; } return { success: false, errors: ['Unknown validation error'] }; } } /** * Validate dual schedule configuration */ public validateDualScheduleConfig(config: unknown): ValidationResult> { try { const validatedConfig = DualScheduleConfigurationSchema.parse(config); return { success: true, data: validatedConfig }; } catch (error) { if (error instanceof z.ZodError) { return { success: false, errors: error.errors.map(e => `${e.path.join('.')}: ${e.message}`) }; } return { success: false, errors: ['Unknown validation error'] }; } } /** * Validate time string format */ public validateTimeString(time: string): ValidationResult { try { const timeSchema = z.string() .regex(/^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/, 'Invalid time format. Use HH:MM') .refine((time) => { const [hours, minutes] = time.split(':').map(Number); return hours >= 0 && hours <= 23 && minutes >= 0 && minutes <= 59; }, 'Invalid time values. Hour must be 0-23, minute must be 0-59'); const validatedTime = timeSchema.parse(time); return { success: true, data: validatedTime }; } catch (error) { if (error instanceof z.ZodError) { return { success: false, errors: error.errors.map(e => e.message) }; } return { success: false, errors: ['Unknown validation error'] }; } } /** * Validate timezone string */ public validateTimezone(timezone: string): ValidationResult { try { const timezoneSchema = z.string() .min(1, 'Timezone is required') .refine((tz) => { try { Intl.DateTimeFormat(undefined, { timeZone: tz }); return true; } catch { return false; } }, 'Invalid timezone identifier'); const validatedTimezone = timezoneSchema.parse(timezone); return { success: true, data: validatedTimezone }; } catch (error) { if (error instanceof z.ZodError) { return { success: false, errors: error.errors.map(e => e.message) }; } return { success: false, errors: ['Unknown validation error'] }; } } /** * Get validation schema for a specific type */ public getSchema(type: 'notification' | 'reminder' | 'contentFetch' | 'userNotification' | 'dualSchedule') { switch (type) { case 'notification': return NotificationOptionsSchema; case 'reminder': return ReminderOptionsSchema; case 'contentFetch': return ContentFetchConfigSchema; case 'userNotification': return UserNotificationConfigSchema; case 'dualSchedule': return DualScheduleConfigurationSchema; default: throw new Error(`Unknown schema type: ${type}`); } } } /** * Enhanced DailyNotificationPlugin with validation */ export class ValidatedDailyNotificationPlugin { private validationService: NotificationValidationService; constructor() { this.validationService = NotificationValidationService.getInstance(); } /** * Schedule daily notification with validation */ async scheduleDailyNotification(options: unknown): Promise { const validation = this.validationService.validateNotificationOptions(options); if (!validation.success) { throw new Error(`Validation failed: ${validation.errors?.join(', ')}`); } // Call native implementation with validated data return await this.nativeScheduleDailyNotification(validation.data!); } /** * Schedule daily reminder with validation */ async scheduleDailyReminder(options: unknown): Promise { const validation = this.validationService.validateReminderOptions(options); if (!validation.success) { throw new Error(`Validation failed: ${validation.errors?.join(', ')}`); } // Call native implementation with validated data return await this.nativeScheduleDailyReminder(validation.data!); } /** * Schedule content fetch with validation */ async scheduleContentFetch(config: unknown): Promise { const validation = this.validationService.validateContentFetchConfig(config); if (!validation.success) { throw new Error(`Validation failed: ${validation.errors?.join(', ')}`); } // Call native implementation with validated data return await this.nativeScheduleContentFetch(validation.data!); } /** * Schedule user notification with validation */ async scheduleUserNotification(config: unknown): Promise { const validation = this.validationService.validateUserNotificationConfig(config); if (!validation.success) { throw new Error(`Validation failed: ${validation.errors?.join(', ')}`); } // Call native implementation with validated data return await this.nativeScheduleUserNotification(validation.data!); } /** * Schedule dual notification with validation */ async scheduleDualNotification(config: unknown): Promise { const validation = this.validationService.validateDualScheduleConfig(config); if (!validation.success) { throw new Error(`Validation failed: ${validation.errors?.join(', ')}`); } // Call native implementation with validated data return await this.nativeScheduleDualNotification(validation.data!); } // Native implementation methods (to be implemented) private async nativeScheduleDailyNotification(options: z.infer): Promise { // Implementation will call the actual plugin throw new Error('Native implementation not yet connected'); } private async nativeScheduleDailyReminder(options: z.infer): Promise { // Implementation will call the actual plugin throw new Error('Native implementation not yet connected'); } private async nativeScheduleContentFetch(config: z.infer): Promise { // Implementation will call the actual plugin throw new Error('Native implementation not yet connected'); } private async nativeScheduleUserNotification(config: z.infer): Promise { // Implementation will call the actual plugin throw new Error('Native implementation not yet connected'); } private async nativeScheduleDualNotification(config: z.infer): Promise { // Implementation will call the actual plugin throw new Error('Native implementation not yet connected'); } } // Export schemas and service export { NotificationOptionsSchema, ReminderOptionsSchema, ContentFetchConfigSchema, UserNotificationConfigSchema, DualScheduleConfigurationSchema }; export default NotificationValidationService;