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
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;
|
|
|