You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

549 lines
16 KiB

/**
* 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<T> {
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<z.infer<typeof NotificationOptionsSchema>> {
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<z.infer<typeof ReminderOptionsSchema>> {
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<z.infer<typeof ContentFetchConfigSchema>> {
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<z.infer<typeof UserNotificationConfigSchema>> {
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<z.infer<typeof DualScheduleConfigurationSchema>> {
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<string> {
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<string> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<typeof NotificationOptionsSchema>): Promise<void> {
// Implementation will call the actual plugin
throw new Error('Native implementation not yet connected');
}
private async nativeScheduleDailyReminder(options: z.infer<typeof ReminderOptionsSchema>): Promise<void> {
// Implementation will call the actual plugin
throw new Error('Native implementation not yet connected');
}
private async nativeScheduleContentFetch(config: z.infer<typeof ContentFetchConfigSchema>): Promise<void> {
// Implementation will call the actual plugin
throw new Error('Native implementation not yet connected');
}
private async nativeScheduleUserNotification(config: z.infer<typeof UserNotificationConfigSchema>): Promise<void> {
// Implementation will call the actual plugin
throw new Error('Native implementation not yet connected');
}
private async nativeScheduleDualNotification(config: z.infer<typeof DualScheduleConfigurationSchema>): Promise<void> {
// 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;