Add tier-based scan scheduler and webhook triggers (FRE-4498)
- ScanScheduler: tier-based scheduling (BASIC=24h, PLUS=6h, PREMIUM=1h) - WebhookHandler: HMAC-verified webhook ingestion with SCAN_TRIGGER support - API routes: /scheduler and /webhooks endpoints under /api/v1/darkwatch - Jobs: scheduled scan checker + webhook retry processor via BullMQ - Schema: ScanSchedule, WebhookEvent models; ScanJob.scheduledBy field - Types: ScheduleStatus, WebhookEventType, WebhookTriggerInput - Tests: scheduler lifecycle + webhook signature/processing tests Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
82
packages/shared-notifications/src/services/sms.service.ts
Normal file
82
packages/shared-notifications/src/services/sms.service.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import twilio from 'twilio';
|
||||
import { loadNotificationConfig } from '../config/notification.config';
|
||||
import type { SMSNotification, NotificationResult } from '../types/notification.types';
|
||||
|
||||
const config = loadNotificationConfig();
|
||||
const twilioClient = twilio(
|
||||
config.twilio.accountSid,
|
||||
config.twilio.authToken
|
||||
);
|
||||
|
||||
export class SMSService {
|
||||
private static instance: SMSService;
|
||||
private sentCount = new Map<string, number>();
|
||||
private cleanupInterval: NodeJS.Timeout;
|
||||
|
||||
private constructor() {
|
||||
this.cleanupInterval = setInterval(() => {
|
||||
const now = Date.now();
|
||||
for (const [key, timestamp] of this.sentCount.entries()) {
|
||||
if (now - timestamp > 60000) {
|
||||
this.sentCount.delete(key);
|
||||
}
|
||||
}
|
||||
}, 60000);
|
||||
}
|
||||
|
||||
static getInstance(): SMSService {
|
||||
if (!SMSService.instance) {
|
||||
SMSService.instance = new SMSService();
|
||||
}
|
||||
return SMSService.instance;
|
||||
}
|
||||
|
||||
async send(notification: SMSNotification): Promise<NotificationResult> {
|
||||
const rateLimitKey = `sms:${notification.to}`;
|
||||
const currentCount = this.sentCount.get(rateLimitKey) || 0;
|
||||
|
||||
if (currentCount >= config.rateLimits.smsPerMinute) {
|
||||
throw new Error(`SMS rate limit exceeded for ${notification.to}`);
|
||||
}
|
||||
|
||||
try {
|
||||
const message = await twilioClient.messages.create({
|
||||
body: notification.body,
|
||||
from: notification.from || config.twilio.messagingServiceSid,
|
||||
to: notification.to,
|
||||
metadata: notification.metadata,
|
||||
});
|
||||
|
||||
this.sentCount.set(rateLimitKey, currentCount + 1);
|
||||
|
||||
return {
|
||||
notificationId: `sms-${message.sid}`,
|
||||
channel: 'sms',
|
||||
status: 'sent',
|
||||
externalId: message.sid,
|
||||
deliveredAt: new Date(),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
notificationId: `sms-${Date.now()}`,
|
||||
channel: 'sms',
|
||||
status: 'failed',
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async sendBatch(notifications: SMSNotification[]): Promise<NotificationResult[]> {
|
||||
const results = await Promise.all(
|
||||
notifications.map(n => this.send(n))
|
||||
);
|
||||
return results;
|
||||
}
|
||||
|
||||
getRateLimitStatus(): { remaining: number; limit: number } {
|
||||
return {
|
||||
remaining: config.rateLimits.smsPerMinute - this.sentCount.size,
|
||||
limit: config.rateLimits.smsPerMinute,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user