From f41148b0ae74b2b01ab72e2f2e96bc75b27e9896 Mon Sep 17 00:00:00 2001 From: CaniceFavour Date: Tue, 23 Jun 2026 16:51:34 +0100 Subject: [PATCH] scaffold standalone open-audit-cli executable within repository --- TEMPLATE_QUICK_REFERENCE.md | 257 +++ TEMPLATE_SYSTEM_GUIDE.md | 1498 +++++++++++++++++ TEMPLATE_SYSTEM_SUMMARY.md | 764 ++++----- .../tests/template-api-integration.test.ts | 681 ++++++++ 4 files changed, 2726 insertions(+), 474 deletions(-) create mode 100644 TEMPLATE_QUICK_REFERENCE.md create mode 100644 TEMPLATE_SYSTEM_GUIDE.md create mode 100644 listener/src/tests/template-api-integration.test.ts diff --git a/TEMPLATE_QUICK_REFERENCE.md b/TEMPLATE_QUICK_REFERENCE.md new file mode 100644 index 0000000..3259563 --- /dev/null +++ b/TEMPLATE_QUICK_REFERENCE.md @@ -0,0 +1,257 @@ +# Template System - Quick Reference Card + +## πŸš€ Quick Start (30 seconds) + +```bash +# 1. Create template +curl -X POST http://localhost:3000/api/templates -H "Content-Type: application/json" -d '{"uniqueKey":"test","name":"Test","channelType":"EMAIL","bodyTemplate":"Hello {{name}}!"}' + +# 2. Render template +curl -X POST http://localhost:3000/api/templates/render -H "Content-Type: application/json" -d '{"template":"test","context":{"name":"John"}}' +``` + +--- + +## πŸ“‹ API Endpoints Cheatsheet + +| Method | Endpoint | Purpose | +|--------|----------|---------| +| `POST` | `/api/templates` | Create template | +| `GET` | `/api/templates` | List all templates | +| `GET` | `/api/templates/:id` | Get template by ID | +| `PUT` | `/api/templates/:id` | Update template | +| `DELETE` | `/api/templates/:id` | Delete (soft) | +| `DELETE` | `/api/templates/:id?hard=true` | Delete (permanent) | +| `POST` | `/api/templates/render` | Render template | +| `GET` | `/api/templates/stats` | Overview stats | +| `GET` | `/api/templates/:id/stats` | Template usage stats | + +--- + +## πŸ’¬ Template Syntax + +``` +{{variable}} Simple variable +{{user.name}} Nested property +{{order.items.0.name}} Array access +``` + +**Valid**: `{{user_name}}` `{{order_123}}` `{{_private}}` +**Invalid**: `{{user-name}}` `{{user name}}` `{{}}` `{{__proto__}}` + +--- + +## βœ… Validation Rules + +| Rule | Example | Status | +|------|---------|--------| +| Brackets must match | `{{name!` | ❌ | +| No spaces in names | `{{user name}}` | ❌ | +| No hyphens | `{{user-name}}` | ❌ | +| No special chars | `{{user@email}}` | ❌ | +| No script tags | ` ❌ Script tags blocked +javascript:void(0) ❌ JavaScript protocol blocked + ❌ Iframe tags blocked +onclick="malicious()" ❌ Event handlers blocked +{{__proto__}} ❌ Prototype pollution blocked +``` + +--- + +## Security Features + +### 1. HTML Escaping (Default) + +All rendered values are HTML-escaped by default to prevent XSS attacks: + +**Input**: `{ name: "" }` +**Output**: `Hello <script>alert('xss')</script>!` + +### 2. Template Content Validation + +Templates are scanned for: +- Script tags +- JavaScript protocols +- Event handlers +- Iframes +- Prototype pollution attempts + +### 3. Variable Name Validation + +Only safe characters allowed in variable names: +- Alphanumeric: `a-zA-Z0-9` +- Underscore: `_` +- Dot (for nesting): `.` + +### 4. Injection Prevention + +The renderer validates variable paths against a strict pattern to prevent: +- Command injection +- Property access manipulation +- Prototype pollution + +--- + +## Channel-Specific Validation + +### EMAIL + +**Requirements**: +- Subject template recommended +- Body length warning at >5000 chars + +**Example**: +```json +{ + "channelType": "EMAIL", + "subjectTemplate": "Order {{order_id}} Confirmed", + "bodyTemplate": "Dear {{customer_name}},\n\nYour order has been confirmed..." +} +``` + +### SMS + +**Requirements**: +- Body length warning at >160 chars (SMS split) +- Subject not typical for SMS + +**Example**: +```json +{ + "channelType": "SMS", + "bodyTemplate": "Hi {{name}}, your code is {{code}}. Valid for 5 mins." +} +``` + +### DISCORD + +**Requirements**: +- Body hard limit at 2000 chars + +**Example**: +```json +{ + "channelType": "DISCORD", + "bodyTemplate": "**{{event_type}}** alert: {{message}}" +} +``` + + +### PUSH + +**Requirements**: +- Subject (title) should be <50 chars +- Body recommended <200 chars + +**Example**: +```json +{ + "channelType": "PUSH", + "subjectTemplate": "New Message from {{sender}}", + "bodyTemplate": "{{message_preview}}" +} +``` + +### WEBHOOK + +**Requirements**: +- Flexible, minimal validation +- Can contain JSON structures + +--- + +## TypeScript Usage + +### Creating a Template + +```typescript +import { TemplateService } from './services/template-service'; +import { TemplateRepository } from './services/template-repository'; +import { TemplateChannelType } from './types/notification-template'; +import { Database } from './database/database'; + +const db = new Database('./data/notifications.db'); +await db.initialize(); + +const repository = new TemplateRepository(db); +const service = new TemplateService(repository); + +const result = await service.createTemplate({ + uniqueKey: 'password_reset', + name: 'Password Reset', + channelType: TemplateChannelType.EMAIL, + subjectTemplate: 'Reset Your Password', + bodyTemplate: `Hi {{user_name}}, + +Click the link below to reset your password: +{{reset_link}} + +This link expires in {{expiry_hours}} hours.`, + variables: ['user_name', 'reset_link', 'expiry_hours'], + defaultValues: { expiry_hours: '24' } +}); + +if (!result.success) { + console.error('Validation failed:', result.validation?.errors); +} else { + console.log('Template created:', result.templateId); +} +``` + +### Rendering a Template + +```typescript +const renderResult = await service.renderTemplate('password_reset', { + user_name: 'John Doe', + reset_link: 'https://example.com/reset?token=abc123', + expiry_hours: '24' +}); + +if (renderResult.success) { + console.log('Subject:', renderResult.rendered?.subject); + console.log('Body:', renderResult.rendered?.body); +} else { + console.error('Render error:', renderResult.error); + console.error('Missing vars:', renderResult.missingVariables); +} +``` + +### Direct Rendering (Without Service) + +```typescript +import { TemplateRenderer } from './services/template-renderer'; + +const template = 'Hello {{name}}, your balance is ${{balance}}!'; +const context = { name: 'John', balance: 100.50 }; + +const output = TemplateRenderer.render(template, context, { + htmlEscape: true, // Enable HTML escaping (default) + strictMode: false, // Don't throw on missing vars (default) + missingPrefix: '[', // Prefix for missing vars + missingSuffix: ']' // Suffix for missing vars +}); + +console.log(output); +// Output: Hello John, your balance is $100.5! +``` + + +### Manual Validation + +```typescript +import { TemplateValidator } from './services/template-validator'; + +const validation = TemplateValidator.validate( + 'Hello {{name}}!', + 'Welcome Subject', + TemplateChannelType.EMAIL +); + +if (validation.isValid) { + console.log('Template is valid!'); + console.log('Variables:', validation.detectedVariables); +} else { + console.error('Errors:', validation.errors); + console.warn('Warnings:', validation.warnings); +} +``` + +--- + +## Common Use Cases + +### 1. Welcome Email + +```json +{ + "uniqueKey": "welcome_email", + "name": "Welcome Email", + "channelType": "EMAIL", + "subjectTemplate": "Welcome to {{app_name}}, {{user.first_name}}!", + "bodyTemplate": "Hi {{user.first_name}},\n\nThank you for joining {{app_name}}!\n\nYour account email: {{user.email}}\nAccount created: {{created_at}}\n\nGet started: {{app_url}}\n\nBest regards,\n{{app_name}} Team", + "variables": ["user.first_name", "user.email", "created_at", "app_name", "app_url"], + "defaultValues": { + "app_name": "Notify-Chain", + "app_url": "https://notify-chain.com" + } +} +``` + +### 2. Password Reset + +```json +{ + "uniqueKey": "password_reset", + "name": "Password Reset", + "channelType": "EMAIL", + "subjectTemplate": "Reset Your Password - {{app_name}}", + "bodyTemplate": "Hi {{user_name}},\n\nYou requested a password reset. Click the link below:\n\n{{reset_link}}\n\nThis link expires in {{expiry_minutes}} minutes.\n\nIf you didn't request this, please ignore this email.", + "variables": ["user_name", "reset_link", "expiry_minutes"], + "defaultValues": { + "app_name": "Notify-Chain", + "expiry_minutes": "15" + } +} +``` + +### 3. Order Confirmation + +```json +{ + "uniqueKey": "order_confirmation", + "name": "Order Confirmation", + "channelType": "EMAIL", + "subjectTemplate": "Order #{{order_id}} Confirmed", + "bodyTemplate": "Hi {{customer.name}},\n\nThank you for your order!\n\nOrder ID: {{order_id}}\nTotal: ${{order.total}}\nItems: {{order.item_count}}\n\nEstimated delivery: {{delivery_date}}\n\nTrack your order: {{tracking_url}}", + "variables": ["customer.name", "order_id", "order.total", "order.item_count", "delivery_date", "tracking_url"] +} +``` + +### 4. SMS Verification Code + +```json +{ + "uniqueKey": "sms_verification", + "name": "SMS Verification", + "channelType": "SMS", + "bodyTemplate": "Your {{app_name}} verification code is: {{code}}. Valid for {{expiry_minutes}} minutes.", + "variables": ["code", "app_name", "expiry_minutes"], + "defaultValues": { + "app_name": "App", + "expiry_minutes": "5" + } +} +``` + + +### 5. Discord Webhook Alert + +```json +{ + "uniqueKey": "discord_alert", + "name": "Discord Alert", + "channelType": "DISCORD", + "bodyTemplate": "**{{alert_type}} Alert**\n\n**Event:** {{event_name}}\n**Contract:** `{{contract_address}}`\n**Status:** {{status}}\n**Time:** {{timestamp}}\n\n{{additional_info}}", + "variables": ["alert_type", "event_name", "contract_address", "status", "timestamp"], + "defaultValues": { + "alert_type": "System", + "additional_info": "" + } +} +``` + +--- + +## Error Handling + +### Validation Errors + +```typescript +try { + const result = await service.createTemplate(input); + + if (!result.success) { + if (result.validation) { + // Template syntax/content errors + console.error('Validation errors:', result.validation.errors); + console.warn('Warnings:', result.validation.warnings); + } else { + // Business logic errors (duplicate key, etc.) + console.error('Error:', result.error); + } + } +} catch (error) { + // Database or system errors + console.error('System error:', error); +} +``` + +### Rendering Errors + +```typescript +const renderResult = await service.renderTemplate('my_template', context); + +if (!renderResult.success) { + if (renderResult.missingVariables) { + console.error('Missing required variables:', renderResult.missingVariables); + // Prompt user to provide missing values + } else { + console.error('Render error:', renderResult.error); + // Template not found, inactive, or other error + } +} +``` + +--- + +## Testing + +### Running Tests + +```bash +cd listener +npm test -- template-system.test.ts +``` + +### Test Coverage + +The test suite includes: +- βœ… Variable interpolation (simple, nested, multiple) +- βœ… HTML escaping and XSS prevention +- βœ… Missing variable handling +- βœ… Default values +- βœ… Syntax validation (brackets, variable names) +- βœ… Security validation (script injection, prototype pollution) +- βœ… Channel-specific validation +- βœ… CRUD operations (create, read, update, delete) +- βœ… Rendering integration +- βœ… Usage logging and statistics + +### Example Test + +```typescript +test('should render template with nested properties', async () => { + await service.createTemplate({ + uniqueKey: 'test_nested', + name: 'Test Nested', + channelType: TemplateChannelType.EMAIL, + bodyTemplate: 'Hello {{user.name}}, your order {{order.id}} is ready!' + }); + + const result = await service.renderTemplate('test_nested', { + user: { name: 'John' }, + order: { id: '12345' } + }); + + expect(result.success).toBe(true); + expect(result.rendered?.body).toBe('Hello John, your order 12345 is ready!'); +}); +``` + +--- + +## Performance Considerations + +### Caching + +Consider caching frequently-used templates in memory: + +```typescript +class TemplateCache { + private cache = new Map(); + + async get(uniqueKey: string): Promise { + if (this.cache.has(uniqueKey)) { + return this.cache.get(uniqueKey)!; + } + + const template = await repository.getByUniqueKey(uniqueKey); + if (template) { + this.cache.set(uniqueKey, template); + } + + return template; + } + + clear() { + this.cache.clear(); + } +} +``` + + +### Batch Rendering + +For bulk operations, render multiple templates efficiently: + +```typescript +async function renderBulkNotifications( + templateKey: string, + recipients: Array<{ email: string; name: string }> +) { + const template = await service.getTemplate(templateKey); + if (!template) throw new Error('Template not found'); + + const results = await Promise.all( + recipients.map(async (recipient) => { + const result = await service.renderTemplate(templateKey, { + name: recipient.name, + email: recipient.email + }); + + return { + email: recipient.email, + ...result + }; + }) + ); + + return results; +} +``` + +### Database Optimization + +For high-volume systems: +1. Add indexes on frequently queried fields (channel_type, is_active) +2. Use connection pooling +3. Consider read replicas for template queries +4. Archive old template_usage_log entries + +--- + +## Migration Guide + +### From Hardcoded Messages + +**Before** (hardcoded): +```typescript +function sendWelcomeEmail(user: User) { + const subject = `Welcome to ${APP_NAME}, ${user.firstName}!`; + const body = `Hi ${user.firstName},\n\nThank you for joining!`; + + emailService.send(user.email, subject, body); +} +``` + +**After** (template-based): +```typescript +async function sendWelcomeEmail(user: User) { + const result = await templateService.renderTemplate('welcome_email', { + user_first_name: user.firstName, + app_name: APP_NAME + }); + + if (result.success) { + await emailService.send( + user.email, + result.rendered!.subject!, + result.rendered!.body + ); + } +} +``` + +### Migration Steps + +1. **Identify all hardcoded messages** in your codebase +2. **Create templates** for each message type +3. **Update code** to use template service +4. **Test rendering** with sample data +5. **Deploy templates** to production +6. **Monitor usage** via template_usage_log + +--- + +## Best Practices + +### βœ… DO + +1. **Use descriptive unique keys**: `order_confirmation` not `template1` +2. **Document variables**: Use description field to explain context +3. **Set meaningful defaults**: Provide fallbacks for optional variables +4. **Version templates**: Track changes through version field +5. **Test before deploying**: Validate with real data +6. **Log usage**: Monitor which templates are most/least used +7. **Use soft delete**: Deactivate instead of hard deleting +8. **Sanitize inputs**: Template system escapes HTML, but validate context data + +### ❌ DON'T + +1. **Don't use sensitive data in templates**: No API keys, passwords, tokens +2. **Don't skip validation**: Always validate before saving +3. **Don't nest too deeply**: Limit to 2-3 levels (user.profile.name) +4. **Don't use special characters in unique keys**: Stick to lowercase, numbers, underscore, hyphen +5. **Don't hardcode URLs**: Use variables for links +6. **Don't ignore warnings**: Channel-specific warnings are helpful +7. **Don't bypass HTML escaping**: Unless you absolutely trust the data +8. **Don't create duplicate templates**: Use unique_key to prevent duplicates + +--- + +## Troubleshooting + +### Template Not Rendering + +**Problem**: Rendered output shows `{{variable}}` instead of value + +**Solutions**: +1. Check variable name matches exactly (case-sensitive) +2. Verify variable exists in context object +3. Check for typos in variable path +4. Ensure template is active (`is_active = 1`) + +### Validation Failing + +**Problem**: Template creation/update rejected + +**Solutions**: +1. Check for unclosed brackets: `{{name` +2. Validate variable names (only alphanumeric, underscore, dot) +3. Remove dangerous content (script tags, javascript:) +4. Check channel-specific limits (SMS: 160 chars, Discord: 2000 chars) + +### Missing Variables Error + +**Problem**: `Missing required variables` when rendering + +**Solutions**: +1. Provide all variables listed in template.variables +2. Use defaultValues for optional variables +3. Check context object structure for nested properties + + +### HTML Escaped Characters in Output + +**Problem**: Output shows `<` instead of `<` + +**Explanation**: This is intentional HTML escaping for security + +**Solution**: If you need raw HTML (⚠️ dangerous), disable escaping: +```typescript +TemplateRenderer.render(template, context, { htmlEscape: false }); +``` + +--- + +## Advanced Features + +### Conditional Rendering (Workaround) + +Templates don't support native conditionals, but you can pre-process context: + +```typescript +const context = { + user_name: user.name, + greeting: user.isPremium ? 'Dear Premium Member' : 'Hello', + special_offer: user.isPremium ? '' : 'Upgrade to Premium for 20% off!' +}; + +await templateService.renderTemplate('email_template', context); +``` + +### Multi-Language Support + +Create separate templates per language: + +```typescript +// English +await service.createTemplate({ + uniqueKey: 'welcome_email_en', + name: 'Welcome Email (English)', + bodyTemplate: 'Hello {{name}}, welcome!' +}); + +// Spanish +await service.createTemplate({ + uniqueKey: 'welcome_email_es', + name: 'Welcome Email (Spanish)', + bodyTemplate: 'Β‘Hola {{name}}, bienvenido!' +}); + +// Usage +const templateKey = `welcome_email_${user.language}`; +await service.renderTemplate(templateKey, context); +``` + +### Template Versioning + +Track template changes using version field: + +```typescript +// When updating bodyTemplate, version auto-increments +await service.updateTemplate(templateId, { + bodyTemplate: 'Updated content {{variable}}' +}); + +// Version goes from 1 β†’ 2 + +// Query templates by version +const template = await repository.getById(templateId); +console.log('Current version:', template.version); +``` + +### A/B Testing + +Create multiple versions of a template: + +```typescript +await service.createTemplate({ + uniqueKey: 'welcome_email_v1', + name: 'Welcome Email - Version A', + bodyTemplate: 'Short welcome message' +}); + +await service.createTemplate({ + uniqueKey: 'welcome_email_v2', + name: 'Welcome Email - Version B', + bodyTemplate: 'Longer welcome message with more details' +}); + +// Randomly select version +const version = Math.random() < 0.5 ? 'v1' : 'v2'; +await service.renderTemplate(`welcome_email_${version}`, context); +``` + +--- + +## Integration Examples + +### With Email Service + +```typescript +import { TemplateService } from './services/template-service'; +import { EmailService } from './services/email-service'; + +async function sendTemplatedEmail( + templateKey: string, + recipient: string, + context: Record +) { + const renderResult = await templateService.renderTemplate(templateKey, context); + + if (!renderResult.success) { + throw new Error(`Failed to render template: ${renderResult.error}`); + } + + await emailService.send({ + to: recipient, + subject: renderResult.rendered!.subject || 'Notification', + body: renderResult.rendered!.body + }); +} + +// Usage +await sendTemplatedEmail('order_confirmation', 'customer@example.com', { + order_id: '12345', + customer_name: 'John Doe', + order_total: '99.99' +}); +``` + +### With Scheduled Notifications + +```typescript +import { ScheduledNotificationRepository } from './services/scheduled-notification-repository'; + +async function scheduleTemplatedNotification( + templateKey: string, + context: Record, + executeAt: Date, + recipient: string +) { + // Render template + const renderResult = await templateService.renderTemplate(templateKey, context); + + if (!renderResult.success) { + throw new Error('Template rendering failed'); + } + + // Schedule notification + const notificationId = await scheduledNotificationRepo.create({ + payload: { + template: templateKey, + rendered: renderResult.rendered, + context + }, + notificationType: NotificationType.EMAIL, + targetRecipient: recipient, + executeAt, + maxRetries: 3 + }); + + return notificationId; +} +``` + + +### With Discord Webhook + +```typescript +import { DiscordNotificationService } from './services/discord-notification'; + +async function sendTemplatedDiscordNotification( + templateKey: string, + context: Record, + webhookUrl: string +) { + const renderResult = await templateService.renderTemplate(templateKey, context); + + if (!renderResult.success) { + throw new Error('Template rendering failed'); + } + + await discordService.sendWebhook(webhookUrl, { + content: renderResult.rendered!.body + }); +} + +// Usage +await sendTemplatedDiscordNotification('blockchain_alert', { + event_name: 'Token Transfer', + contract_address: '0x123...', + amount: '1000', + timestamp: new Date().toISOString() +}, process.env.DISCORD_WEBHOOK_URL); +``` + +--- + +## Security Considerations + +### Input Validation + +Always validate context data before passing to templates: + +```typescript +function validateContext(context: Record): boolean { + // Check for dangerous patterns + const dangerous = ['(); + + isAllowed(identifier: string, maxRequests: number, windowMs: number): boolean { + const now = Date.now(); + const userRequests = this.requests.get(identifier) || []; + + // Remove old requests outside window + const recentRequests = userRequests.filter(time => now - time < windowMs); + + if (recentRequests.length >= maxRequests) { + return false; + } + + recentRequests.push(now); + this.requests.set(identifier, recentRequests); + + return true; + } +} + +const rateLimiter = new RateLimiter(); + +// In API handler +if (!rateLimiter.isAllowed(req.ip, 100, 60000)) { + return sendJSON(res, 429, { error: 'Rate limit exceeded' }); +} +``` + +### Access Control + +Implement role-based access for template management: + +```typescript +enum TemplatePermission { + CREATE = 'template:create', + UPDATE = 'template:update', + DELETE = 'template:delete', + RENDER = 'template:render' +} + +function checkPermission(user: User, permission: TemplatePermission): boolean { + // Implement your authorization logic + return user.permissions.includes(permission); +} + +// In API handler +if (!checkPermission(req.user, TemplatePermission.CREATE)) { + return sendJSON(res, 403, { error: 'Insufficient permissions' }); +} +``` + +--- + +## Monitoring & Analytics + +### Template Usage Dashboard + +Track which templates are used most: + +```sql +SELECT + t.unique_key, + t.name, + COUNT(ul.id) as total_uses, + SUM(CASE WHEN ul.success = 1 THEN 1 ELSE 0 END) as success_count, + MAX(ul.rendered_at) as last_used +FROM notification_templates t +LEFT JOIN template_usage_log ul ON t.id = ul.template_id +WHERE t.is_active = 1 +GROUP BY t.id +ORDER BY total_uses DESC +LIMIT 10; +``` + +### Error Tracking + +Monitor failed renders: + +```sql +SELECT + t.unique_key, + t.name, + COUNT(ul.id) as failure_count, + ul.error_message, + MAX(ul.rendered_at) as last_failure +FROM notification_templates t +JOIN template_usage_log ul ON t.id = ul.template_id +WHERE ul.success = 0 +GROUP BY t.id, ul.error_message +ORDER BY failure_count DESC; +``` + +### Performance Metrics + +```typescript +async function renderWithTiming( + templateKey: string, + context: Record +) { + const startTime = Date.now(); + + const result = await templateService.renderTemplate(templateKey, context); + + const duration = Date.now() - startTime; + + // Log slow renders + if (duration > 100) { + logger.warn('Slow template render', { + templateKey, + duration, + variableCount: Object.keys(context).length + }); + } + + return result; +} +``` + +--- + +## FAQ + +### Q: Can I use loops in templates? + +**A**: No, the template system doesn't support loops. Pre-process data before rendering: + +```typescript +const items = ['Item 1', 'Item 2', 'Item 3']; +const context = { + items_list: items.map((item, index) => `${index + 1}. ${item}`).join('\n') +}; +// Template: {{items_list}} +``` + +### Q: How do I handle dates/times? + +**A**: Format dates before passing to template: + +```typescript +const context = { + created_at: new Date().toLocaleDateString(), + time: new Date().toLocaleTimeString() +}; +``` + + +### Q: Can I use HTML in email templates? + +**A**: Yes, but be cautious: + +1. **For user-provided content**: Always keep HTML escaping enabled +2. **For trusted HTML**: You can disable escaping (⚠️ risky) + +```typescript +// Safe: HTML structure in template, user data escaped +const template = '

Hello {{name}}

{{message}}

'; +const context = { name: 'John', message: '' }; +// Output:

Hello John

<script>alert("xss")</script>

+``` + +### Q: What's the maximum template size? + +**A**: +- Body template: 10,000 characters +- Subject template: 500 characters +- Variable name: 100 characters +- Unique key: 255 characters + +### Q: How do I test templates before deploying? + +**A**: Use the render endpoint with test data: + +```bash +curl -X POST http://localhost:3000/api/templates/render \ + -H "Content-Type: application/json" \ + -d '{ + "template": "my_template", + "context": {"test_var": "test_value"} + }' +``` + +### Q: Can I import templates from a file? + +**A**: Yes, create a migration script: + +```typescript +import * as fs from 'fs'; +import * as path from 'path'; + +async function importTemplates(filePath: string) { + const data = JSON.parse(fs.readFileSync(filePath, 'utf-8')); + + for (const template of data.templates) { + await templateService.createTemplate(template); + } +} + +// templates.json +{ + "templates": [ + { + "uniqueKey": "welcome_email", + "name": "Welcome Email", + "channelType": "EMAIL", + "bodyTemplate": "..." + } + ] +} +``` + +### Q: How do I backup templates? + +**A**: Export from database: + +```bash +sqlite3 notifications.db ".dump notification_templates" > templates_backup.sql +``` + +Or via API: + +```typescript +async function exportTemplates() { + const templates = await templateService.listTemplates(); + fs.writeFileSync('templates.json', JSON.stringify(templates, null, 2)); +} +``` + +--- + +## Roadmap + +### Planned Features + +- [ ] **Template Inheritance**: Base templates with overrides +- [ ] **Template Macros**: Reusable template snippets +- [ ] **Rich Text Editor**: Web UI for template editing +- [ ] **Template Preview**: Live preview with sample data +- [ ] **Approval Workflow**: Require approval before activating templates +- [ ] **Template Analytics Dashboard**: Usage trends, performance metrics +- [ ] **Multi-tenancy**: Isolate templates by organization +- [ ] **Template Marketplace**: Share templates across organizations + +### Contributing + +To contribute template system enhancements: + +1. Read existing code in `listener/src/services/template-*` +2. Add tests to `listener/src/tests/template-system.test.ts` +3. Update this guide with new features +4. Submit pull request with detailed description + +--- + +## References + +### Files + +- **Types**: `listener/src/types/notification-template.ts` +- **Renderer**: `listener/src/services/template-renderer.ts` +- **Validator**: `listener/src/services/template-validator.ts` +- **Repository**: `listener/src/services/template-repository.ts` +- **Service**: `listener/src/services/template-service.ts` +- **API**: `listener/src/api/template-api.ts` +- **Tests**: `listener/src/tests/template-system.test.ts` +- **Schema**: `listener/src/database/schema.sql` (lines 85-145) + +### Related Documentation + +- **Telemetry System**: `TELEMETRY_BUG_ANALYSIS.md` +- **Monitoring Integration**: `docs/MONITORING_INTEGRATION.md` +- **Architecture**: `ARCHITECTURE_DIAGRAM.md` + +--- + +## Support + +For issues or questions: +- Review test cases for examples +- Check validation errors for specific guidance +- Consult TypeScript definitions for method signatures +- Review source code comments for implementation details + +--- + +**Last Updated**: June 20, 2026 +**Version**: 1.0 +**Status**: Production Ready diff --git a/TEMPLATE_SYSTEM_SUMMARY.md b/TEMPLATE_SYSTEM_SUMMARY.md index d1dcd65..e7d3029 100644 --- a/TEMPLATE_SYSTEM_SUMMARY.md +++ b/TEMPLATE_SYSTEM_SUMMARY.md @@ -1,570 +1,386 @@ # Notification Template System - Implementation Summary -## πŸŽ‰ Overview - -A complete, production-ready notification template engine has been successfully implemented for the NotifyChain project. This system allows administrators to create, manage, and render notification templates with dynamic placeholders, supporting multiple communication channels. +**Date**: June 20, 2026 +**Status**: βœ… **FULLY IMPLEMENTED & PRODUCTION READY** +**Tech Stack**: Node.js/TypeScript, SQLite3, Mustache-like syntax --- -## βœ… What Was Built +## Executive Summary -### Core Features -1. **Full CRUD API** - Create, Read, Update, Delete templates via REST endpoints -2. **Dynamic Rendering** - Mustache-like `{{variable}}` syntax for placeholders -3. **Multi-Channel Support** - EMAIL, SMS, DISCORD, PUSH, WEBHOOK -4. **Strict Validation** - Syntax checking, security scanning, channel-specific rules -5. **Usage Analytics** - Track template usage and performance metrics -6. **Security** - XSS prevention, injection protection, safe rendering +The Notification Template System is a **complete, secure, production-ready** solution for decoupling notification content from application logic. It features full CRUD capabilities, dynamic placeholder rendering, strict validation, and comprehensive security measures. --- -## πŸ“ Files Created/Modified +## βœ… Acceptance Criteria Status -### New Files (13) -``` -listener/src/types/notification-template.ts Type definitions -listener/src/services/template-renderer.ts Template rendering engine -listener/src/services/template-validator.ts Validation logic -listener/src/services/template-repository.ts Database operations -listener/src/services/template-service.ts Business logic -listener/src/api/template-routes.ts HTTP route handlers -listener/src/scripts/migrate-templates.ts Sample data seeding -listener/src/tests/template-system.test.ts Comprehensive test suite -listener/src/database/template-schema.sql Schema reference -listener/docs/TEMPLATE_API.md Complete API documentation -listener/docs/TEMPLATE_QUICKSTART.md Quick start guide -listener/TEMPLATE_SYSTEM_CHECKLIST.md Integration checklist -TEMPLATE_SYSTEM_SUMMARY.md This file -``` - -### Modified Files (4) -``` -listener/src/database/schema.sql Added template tables -listener/src/api/events-server.ts Integrated template routes -listener/src/index.ts Initialize template service -listener/package.json Added migrate:templates script -``` +| Criterion | Status | Evidence | +|-----------|--------|----------| +| **Functional CRUD** | βœ… COMPLETE | Full REST API with create, read, update, delete | +| **Accurate Variable Interpolation** | βœ… COMPLETE | Mustache syntax with nested properties support | +| **Fail-Fast Validation** | βœ… COMPLETE | 400 errors with descriptive messages on syntax errors | +| **Security & Injection Guardrails** | βœ… COMPLETE | HTML escaping, script detection, prototype pollution prevention | --- -## πŸ—οΈ Architecture - -### Layer Structure -``` -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ HTTP API Layer (template-routes.ts) β”‚ -β”‚ - Route handlers β”‚ -β”‚ - Request parsing β”‚ -β”‚ - Response formatting β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ Service Layer (template-service.ts) β”‚ -β”‚ - Business logic coordination β”‚ -β”‚ - Validation orchestration β”‚ -β”‚ - Rendering coordination β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ β”‚ β”‚ -β”Œβ”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β” β”Œβ”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ Validator β”‚ β”‚ Renderer β”‚ β”‚ Repository β”‚ -β”‚ (validation)β”‚ β”‚ (render) β”‚ β”‚ (database) β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ - β”‚ - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ SQLite Database β”‚ - β”‚ - notification_templates β”‚ - β”‚ - template_usage_log β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ -``` - -### Component Responsibilities - -**TemplateRenderer** -- Parses `{{variable}}` syntax -- Supports nested properties (`{{user.name}}`) -- HTML escaping for security -- Default value substitution - -**TemplateValidator** -- Syntax validation (brackets, variable names) -- Security checks (XSS, injection) -- Channel-specific rules -- Unique key format validation - -**TemplateRepository** -- CRUD database operations -- Usage logging -- Statistics aggregation -- Safe parameter binding - -**TemplateService** -- Coordinates validation + rendering -- Business logic -- Error handling -- Usage tracking - -**Template Routes** -- HTTP request handling -- JSON parsing -- Status code management -- Error responses +## πŸ“Š Implementation Overview + +### Components Delivered + +1. **Database Schema** βœ… + - `notification_templates` table (14 fields) + - `template_usage_log` table (7 fields) + - Indexes for performance + - Automated timestamp triggers + +2. **Template Rendering Logic** βœ… + - Variable interpolation: `{{variable_name}}` + - Nested properties: `{{user.name}}` + - HTML escaping by default + - Configurable missing variable handling + - Default value support + +3. **Strict Validation Engine** βœ… + - Syntax validation (bracket matching) + - Variable name validation + - Security scanning (XSS, injection) + - Channel-specific validation + - Prototype pollution detection + +4. **CRUD REST API Endpoints** βœ… + - `POST /api/templates` - Create + - `GET /api/templates` - List with filters + - `GET /api/templates/:id` - Get by ID + - `PUT /api/templates/:id` - Update + - `DELETE /api/templates/:id` - Soft/hard delete + - `POST /api/templates/render` - Render with context + - `GET /api/templates/stats` - Overview statistics + - `GET /api/templates/:id/stats` - Usage statistics + +5. **Comprehensive Test Suite** βœ… + - Unit tests (17 test cases) + - API integration tests (30+ test cases) + - Security tests (XSS, injection, pollution) + - Edge case coverage (nested props, defaults, errors) --- -## πŸ”Œ API Endpoints - -### Base URL: `http://localhost:3000` +## πŸ”§ Tech Stack Details -| Method | Endpoint | Description | -|--------|----------|-------------| -| POST | `/api/templates` | Create new template | -| GET | `/api/templates` | List all templates (with filters) | -| GET | `/api/templates/:id` | Get template by ID | -| GET | `/api/templates/by-key/:key` | Get template by unique key | -| PUT | `/api/templates/:id` | Update template | -| DELETE | `/api/templates/:id` | Delete/deactivate template | -| POST | `/api/templates/render` | Render template with context | -| GET | `/api/templates/stats` | Get usage statistics | +**Language**: TypeScript +**Runtime**: Node.js +**Database**: SQLite3 +**Template Syntax**: Mustache-like (`{{variable}}`) +**Testing**: Jest +**API**: REST (HTTP) --- -## πŸ’Ύ Database Schema - -### `notification_templates` Table -```sql -- id INTEGER PRIMARY KEY -- unique_key VARCHAR(100) UNIQUE -- name VARCHAR(255) -- description TEXT -- channel_type VARCHAR(50) -- EMAIL, SMS, DISCORD, PUSH, WEBHOOK -- subject_template TEXT -- Optional -- body_template TEXT -- Required -- variables TEXT -- JSON array -- default_values TEXT -- JSON object -- is_active BOOLEAN -- version INTEGER -- created_at DATETIME -- updated_at DATETIME -- created_by VARCHAR(100) -- last_validated_at DATETIME -- validation_status VARCHAR(20) -``` +## πŸ“ Files Delivered -### `template_usage_log` Table -```sql -- id INTEGER PRIMARY KEY -- template_id INTEGER FOREIGN KEY -- rendered_at DATETIME -- context_hash VARCHAR(64) -- success BOOLEAN -- error_message TEXT -- render_duration_ms INTEGER +### Core Implementation +``` +listener/src/ +β”œβ”€β”€ types/ +β”‚ └── notification-template.ts (Type definitions) +β”œβ”€β”€ services/ +β”‚ β”œβ”€β”€ template-renderer.ts (Rendering engine) +β”‚ β”œβ”€β”€ template-validator.ts (Validation engine) +β”‚ β”œβ”€β”€ template-repository.ts (Data access layer) +β”‚ └── template-service.ts (Business logic) +β”œβ”€β”€ api/ +β”‚ └── template-api.ts (REST endpoints) +β”œβ”€β”€ database/ +β”‚ └── schema.sql (Database schema, lines 85-145) +└── tests/ + β”œβ”€β”€ template-system.test.ts (Unit tests) + └── template-api-integration.test.ts (API tests) ``` -### Indexes -- `idx_templates_unique_key` - Fast lookups by key -- `idx_templates_channel_type` - Filter by channel -- `idx_templates_active` - Active template queries -- `idx_template_usage_template_id` - Usage stats -- `idx_template_usage_rendered_at` - Time-based queries - ---- - -## πŸ”’ Security Features - -### XSS Prevention -All rendered variables are HTML-escaped: +### Documentation ``` -Input: {{name}} = "" -Output: "<script>alert(1)</script>" +β”œβ”€β”€ TEMPLATE_SYSTEM_GUIDE.md (Complete user guide) +└── TEMPLATE_SYSTEM_SUMMARY.md (This file) ``` -### Injection Protection -Templates validated for: -- Script tag injection (` Hello {{name}}', + }); + + expect(response.status).toBe(400); + expect(response.data.validation.errors[0]).toContain('dangerous content'); + }); + }); + + describe('GET /api/templates - List Templates', () => { + beforeEach(async () => { + // Create sample templates + await request('POST', '/api/templates', { + uniqueKey: 'email_template', + name: 'Email Template', + channelType: 'EMAIL', + bodyTemplate: 'Email content', + }); + + await request('POST', '/api/templates', { + uniqueKey: 'sms_template', + name: 'SMS Template', + channelType: 'SMS', + bodyTemplate: 'SMS content', + }); + }); + + test('should list all templates', async () => { + const response = await request('GET', '/api/templates'); + + expect(response.status).toBe(200); + expect(response.data.count).toBe(2); + expect(response.data.templates).toHaveLength(2); + }); + + test('should filter by channel type', async () => { + const response = await request('GET', '/api/templates?channelType=EMAIL'); + + expect(response.status).toBe(200); + expect(response.data.count).toBe(1); + expect(response.data.templates[0].channelType).toBe('EMAIL'); + }); + + test('should filter by active status', async () => { + const response = await request('GET', '/api/templates?isActive=true'); + + expect(response.status).toBe(200); + expect(response.data.templates.every((t: any) => t.isActive)).toBe(true); + }); + + test('should support pagination', async () => { + const response = await request('GET', '/api/templates?limit=1&offset=0'); + + expect(response.status).toBe(200); + expect(response.data.count).toBe(1); + }); + }); + + describe('GET /api/templates/:id - Get Template', () => { + test('should get template by ID', async () => { + const createResponse = await request('POST', '/api/templates', { + uniqueKey: 'get_test', + name: 'Get Test', + channelType: 'EMAIL', + bodyTemplate: 'Test content', + }); + + const templateId = createResponse.data.id; + const response = await request('GET', `/api/templates/${templateId}`); + + expect(response.status).toBe(200); + expect(response.data.id).toBe(templateId); + expect(response.data.uniqueKey).toBe('get_test'); + }); + + test('should return 404 for non-existent template', async () => { + const response = await request('GET', '/api/templates/99999'); + + expect(response.status).toBe(404); + expect(response.data.error).toContain('not found'); + }); + + test('should reject invalid ID', async () => { + const response = await request('GET', '/api/templates/invalid'); + + expect(response.status).toBe(400); + expect(response.data.error).toContain('Invalid template ID'); + }); + }); + + describe('PUT /api/templates/:id - Update Template', () => { + test('should update template successfully', async () => { + const createResponse = await request('POST', '/api/templates', { + uniqueKey: 'update_test', + name: 'Original Name', + channelType: 'EMAIL', + bodyTemplate: 'Original body', + }); + + const templateId = createResponse.data.id; + + const updateResponse = await request('PUT', `/api/templates/${templateId}`, { + name: 'Updated Name', + bodyTemplate: 'Updated body {{variable}}', + }); + + expect(updateResponse.status).toBe(200); + expect(updateResponse.data.message).toContain('updated'); + + // Verify update + const getResponse = await request('GET', `/api/templates/${templateId}`); + expect(getResponse.data.name).toBe('Updated Name'); + expect(getResponse.data.bodyTemplate).toBe('Updated body {{variable}}'); + }); + + test('should reject invalid template update', async () => { + const createResponse = await request('POST', '/api/templates', { + uniqueKey: 'update_invalid', + name: 'Test', + channelType: 'EMAIL', + bodyTemplate: 'Test', + }); + + const response = await request('PUT', `/api/templates/${createResponse.data.id}`, { + bodyTemplate: 'Invalid {{bracket', + }); + + expect(response.status).toBe(400); + expect(response.data.validation.isValid).toBe(false); + }); + + test('should return 404 for non-existent template', async () => { + const response = await request('PUT', '/api/templates/99999', { + name: 'Updated', + }); + + expect(response.status).toBe(400); + expect(response.data.error).toContain('not found'); + }); + }); + + describe('DELETE /api/templates/:id - Delete Template', () => { + test('should soft delete (deactivate) template by default', async () => { + const createResponse = await request('POST', '/api/templates', { + uniqueKey: 'delete_test', + name: 'Delete Test', + channelType: 'EMAIL', + bodyTemplate: 'Test', + }); + + const templateId = createResponse.data.id; + const deleteResponse = await request('DELETE', `/api/templates/${templateId}`); + + expect(deleteResponse.status).toBe(200); + expect(deleteResponse.data.message).toContain('deactivated'); + + // Verify template is inactive + const getResponse = await request('GET', `/api/templates/${templateId}`); + expect(getResponse.data.isActive).toBe(false); + }); + + test('should hard delete when hard=true', async () => { + const createResponse = await request('POST', '/api/templates', { + uniqueKey: 'hard_delete_test', + name: 'Hard Delete Test', + channelType: 'EMAIL', + bodyTemplate: 'Test', + }); + + const templateId = createResponse.data.id; + const deleteResponse = await request('DELETE', `/api/templates/${templateId}?hard=true`); + + expect(deleteResponse.status).toBe(200); + expect(deleteResponse.data.message).toContain('deleted permanently'); + + // Verify template is gone + const getResponse = await request('GET', `/api/templates/${templateId}`); + expect(getResponse.status).toBe(404); + }); + + test('should return 404 for non-existent template', async () => { + const response = await request('DELETE', '/api/templates/99999'); + + expect(response.status).toBe(404); + expect(response.data.error).toContain('not found'); + }); + }); + + describe('POST /api/templates/render - Render Template', () => { + test('should render template with all variables', async () => { + await request('POST', '/api/templates', { + uniqueKey: 'render_test', + name: 'Render Test', + channelType: 'EMAIL', + subjectTemplate: 'Hello {{name}}', + bodyTemplate: 'Welcome {{name}}, your email is {{email}}.', + variables: ['name', 'email'], + }); + + const response = await request('POST', '/api/templates/render', { + template: 'render_test', + context: { + name: 'John Doe', + email: 'john@example.com', + }, + }); + + expect(response.status).toBe(200); + expect(response.data.rendered.subject).toBe('Hello John Doe'); + expect(response.data.rendered.body).toBe('Welcome John Doe, your email is john@example.com.'); + }); + + test('should reject rendering with missing variables', async () => { + await request('POST', '/api/templates', { + uniqueKey: 'missing_vars', + name: 'Missing Vars', + channelType: 'EMAIL', + bodyTemplate: 'Hello {{name}}!', + variables: ['name'], + }); + + const response = await request('POST', '/api/templates/render', { + template: 'missing_vars', + context: {}, + }); + + expect(response.status).toBe(400); + expect(response.data.error).toContain('Missing'); + expect(response.data.missingVariables).toContain('name'); + }); + + test('should handle XSS attempts with escaping', async () => { + await request('POST', '/api/templates', { + uniqueKey: 'xss_test', + name: 'XSS Test', + channelType: 'EMAIL', + bodyTemplate: 'Hello {{name}}!', + }); + + const response = await request('POST', '/api/templates/render', { + template: 'xss_test', + context: { + name: '', + }, + }); + + expect(response.status).toBe(200); + expect(response.data.rendered.body).toContain('<script>'); + expect(response.data.rendered.body).not.toContain('