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:
@@ -1,6 +1,6 @@
|
||||
import { Queue, Worker, QueueScheduler } from "bullmq";
|
||||
import { Queue, Worker } from "bullmq";
|
||||
import { Redis } from "ioredis";
|
||||
import { ScanService } from "@shieldai/darkwatch";
|
||||
import { ScanService, ScanScheduler, WebhookHandler } from "@shieldai/darkwatch";
|
||||
import { AlertPipeline } from "@shieldai/darkwatch";
|
||||
|
||||
const redisUrl = process.env.REDIS_URL || "redis://localhost:6379";
|
||||
@@ -8,6 +8,7 @@ const connection = new Redis(redisUrl);
|
||||
|
||||
const scanQueue = new Queue("darkwatch-scans", { connection });
|
||||
const alertQueue = new Queue("darkwatch-alerts", { connection });
|
||||
const scheduleQueue = new Queue("darkwatch-scheduler", { connection });
|
||||
|
||||
const scanWorker = new Worker(
|
||||
"darkwatch-scans",
|
||||
@@ -30,22 +31,77 @@ const alertWorker = new Worker(
|
||||
{ connection, concurrency: 1 }
|
||||
);
|
||||
|
||||
const scheduler = new QueueScheduler("darkwatch-alerts", { connection });
|
||||
const scheduleWorker = new Worker(
|
||||
"darkwatch-scheduler",
|
||||
async () => {
|
||||
const scheduler = new ScanScheduler();
|
||||
const dueSchedules = await scheduler.getDueSchedules();
|
||||
const results: Array<{ userId: string; queued: boolean }> = [];
|
||||
|
||||
for (const schedule of dueSchedules) {
|
||||
try {
|
||||
await scanQueue.add("scheduled-scan", {
|
||||
userId: schedule.userId,
|
||||
source: undefined,
|
||||
}, {
|
||||
attempts: 3,
|
||||
backoff: { type: "exponential", delay: 5000 },
|
||||
jobId: `scheduled-scan-${schedule.userId}-${Date.now()}`,
|
||||
});
|
||||
|
||||
await scheduler.markScanned(schedule.userId);
|
||||
results.push({ userId: schedule.userId, queued: true });
|
||||
} catch (err) {
|
||||
console.error(`[Scheduler] Failed to queue scan for ${schedule.userId}:`, err);
|
||||
results.push({ userId: schedule.userId, queued: false });
|
||||
}
|
||||
}
|
||||
|
||||
return { processed: results.length, completedAt: new Date().toISOString() };
|
||||
},
|
||||
{ connection, concurrency: 1 }
|
||||
);
|
||||
|
||||
const webhookWorker = new Worker(
|
||||
"darkwatch-webhooks",
|
||||
async () => {
|
||||
const handler = new WebhookHandler();
|
||||
const processed = await handler.processPendingEvents();
|
||||
return { processed, completedAt: new Date().toISOString() };
|
||||
},
|
||||
{ connection, concurrency: 1 }
|
||||
);
|
||||
|
||||
scanWorker.on("completed", (job) => {
|
||||
console.log(`[Scan] Job ${job.id} completed: ${JSON.stringify(job.returnvalue)}`);
|
||||
console.log(`[Scan] Job ${job?.id} completed: ${JSON.stringify(job?.returnvalue)}`);
|
||||
});
|
||||
|
||||
scanWorker.on("failed", (job, err) => {
|
||||
console.error(`[Scan] Job ${job.id} failed: ${err.message}`);
|
||||
console.error(`[Scan] Job ${job?.id} failed: ${err.message}`);
|
||||
});
|
||||
|
||||
alertWorker.on("completed", (job) => {
|
||||
console.log(`[Alert] Job ${job.id} completed: ${JSON.stringify(job.returnvalue)}`);
|
||||
console.log(`[Alert] Job ${job?.id} completed: ${JSON.stringify(job?.returnvalue)}`);
|
||||
});
|
||||
|
||||
alertWorker.on("failed", (job, err) => {
|
||||
console.error(`[Alert] Job ${job.id} failed: ${err.message}`);
|
||||
console.error(`[Alert] Job ${job?.id} failed: ${err.message}`);
|
||||
});
|
||||
|
||||
scheduleWorker.on("completed", (job) => {
|
||||
console.log(`[Scheduler] Job ${job?.id} completed: ${JSON.stringify(job?.returnvalue)}`);
|
||||
});
|
||||
|
||||
scheduleWorker.on("failed", (job, err) => {
|
||||
console.error(`[Scheduler] Job ${job?.id} failed: ${err.message}`);
|
||||
});
|
||||
|
||||
webhookWorker.on("completed", (job) => {
|
||||
console.log(`[Webhook] Job ${job?.id} completed: ${JSON.stringify(job?.returnvalue)}`);
|
||||
});
|
||||
|
||||
webhookWorker.on("failed", (job, err) => {
|
||||
console.error(`[Webhook] Job ${job?.id} failed: ${err.message}`);
|
||||
});
|
||||
|
||||
export async function addScanJob(userId: string, source?: string) {
|
||||
@@ -63,7 +119,19 @@ export async function scheduleAlertProcessing() {
|
||||
});
|
||||
}
|
||||
|
||||
scanWorker.on("waiting", () => console.log("[Worker] Scan worker ready"));
|
||||
alertWorker.on("waiting", () => console.log("[Worker] Alert worker ready"));
|
||||
export async function schedulePeriodicScanCheck() {
|
||||
return scheduleQueue.add("check-due-scans", {}, {
|
||||
repeat: { pattern: "*/10 * * * *" },
|
||||
jobId: "scheduler-recurring",
|
||||
});
|
||||
}
|
||||
|
||||
export async function scheduleWebhookProcessor() {
|
||||
const webhookQueue = new Queue("darkwatch-webhooks", { connection });
|
||||
return webhookQueue.add("process-pending-webhooks", {}, {
|
||||
repeat: { pattern: "*/2 * * * *" },
|
||||
jobId: "webhook-processor-recurring",
|
||||
});
|
||||
}
|
||||
|
||||
console.log("Job workers started");
|
||||
|
||||
Reference in New Issue
Block a user