Merge FrenoCorp apps into ShieldAI packages/: - packages/api: merged routes (notifications), middleware (auth, rate-limit, error, logging), config, services (darkwatch, spamshield, voiceprint), tests - packages/web: new SolidJS web app stub - packages/mobile: new SolidJS mobile app stub - packages/shared-db: new Prisma DB package (separate from existing packages/db) - pnpm-workspace.yaml: restored (apps/* removed, already covered by packages/*) Next: reconcile packages/shared-db with packages/db, and fix server.ts correlationRoutes import
156 lines
4.0 KiB
TypeScript
156 lines
4.0 KiB
TypeScript
import { prisma, SubscriptionTier, SubscriptionStatus } from '@shieldsai/shared-db';
|
|
import { tierConfig } from '@shieldsai/shared-billing';
|
|
import { darkwatchScanQueue } from '@shieldsai/jobs';
|
|
import { randomUUID } from 'crypto';
|
|
|
|
const CRON_EXPRESSIONS = {
|
|
daily: '0 0 * * *',
|
|
hourly: '0 * * * *',
|
|
realtime: null,
|
|
};
|
|
|
|
export class SchedulerService {
|
|
async scheduleSubscriptionScans() {
|
|
const activeSubscriptions = await prisma.subscription.findMany({
|
|
where: {
|
|
tier: { in: [SubscriptionTier.basic, SubscriptionTier.plus, SubscriptionTier.premium] },
|
|
status: SubscriptionStatus.active,
|
|
},
|
|
select: {
|
|
id: true,
|
|
tier: true,
|
|
userId: true,
|
|
},
|
|
});
|
|
|
|
const jobsEnqueued = [];
|
|
|
|
for (const subscription of activeSubscriptions) {
|
|
const frequency = tierConfig[subscription.tier].features.darkWebScanFrequency;
|
|
const cron = CRON_EXPRESSIONS[frequency];
|
|
|
|
if (!cron) {
|
|
continue;
|
|
}
|
|
|
|
const jobKey = `scheduled-scan:${subscription.id}`;
|
|
|
|
try {
|
|
await darkwatchScanQueue.add(
|
|
'scheduled-scan',
|
|
{
|
|
subscriptionId: subscription.id,
|
|
tier: subscription.tier,
|
|
scanType: 'scheduled',
|
|
},
|
|
{
|
|
jobId: jobKey,
|
|
repeat: {
|
|
every: frequency === 'daily'
|
|
? 24 * 60 * 60 * 1000
|
|
: 60 * 60 * 1000,
|
|
},
|
|
priority: subscription.tier === SubscriptionTier.premium ? 1 : 3,
|
|
}
|
|
);
|
|
|
|
jobsEnqueued.push({
|
|
subscriptionId: subscription.id,
|
|
tier: subscription.tier,
|
|
frequency,
|
|
});
|
|
} catch (error) {
|
|
if ((error as Error).message?.includes('Duplicate')) {
|
|
continue;
|
|
}
|
|
console.error(
|
|
`[SchedulerService] Failed to schedule scan for ${subscription.id}:`,
|
|
error
|
|
);
|
|
}
|
|
}
|
|
|
|
return jobsEnqueued;
|
|
}
|
|
|
|
async enqueueOnDemandScan(subscriptionId: string) {
|
|
const subscription = await prisma.subscription.findUnique({
|
|
where: { id: subscriptionId },
|
|
select: { id: true, tier: true },
|
|
});
|
|
|
|
if (!subscription) {
|
|
throw new Error(`Subscription ${subscriptionId} not found`);
|
|
}
|
|
|
|
return darkwatchScanQueue.add(
|
|
'on-demand-scan',
|
|
{
|
|
subscriptionId,
|
|
tier: subscription.tier,
|
|
scanType: 'on-demand',
|
|
},
|
|
{
|
|
priority: 1,
|
|
jobId: `on-demand-scan:${subscriptionId}:${randomUUID()}`,
|
|
}
|
|
);
|
|
}
|
|
|
|
async enqueueRealtimeTrigger(subscriptionId: string, sourceData: Record<string, unknown>) {
|
|
const subscription = await prisma.subscription.findUnique({
|
|
where: { id: subscriptionId },
|
|
select: { id: true, tier: true },
|
|
});
|
|
|
|
if (!subscription || subscription.tier !== SubscriptionTier.premium) {
|
|
throw new Error('Realtime triggers require Premium tier');
|
|
}
|
|
|
|
return darkwatchScanQueue.add(
|
|
'realtime-trigger',
|
|
{
|
|
subscriptionId,
|
|
tier: subscription.tier,
|
|
scanType: 'realtime',
|
|
sourceData,
|
|
},
|
|
{
|
|
priority: 0,
|
|
jobId: `realtime-trigger:${subscriptionId}:${randomUUID()}`,
|
|
}
|
|
);
|
|
}
|
|
|
|
async rescheduleAll() {
|
|
const repeatableJobs = await darkwatchScanQueue.getRepeatableJobs();
|
|
|
|
for (const job of repeatableJobs) {
|
|
await darkwatchScanQueue.removeRepeatableByKey(job.key);
|
|
}
|
|
|
|
return this.scheduleSubscriptionScans();
|
|
}
|
|
|
|
async getScanSchedule(subscriptionId: string) {
|
|
const subscription = await prisma.subscription.findUnique({
|
|
where: { id: subscriptionId },
|
|
select: { tier: true },
|
|
});
|
|
|
|
if (!subscription) return null;
|
|
|
|
const frequency = tierConfig[subscription.tier].features.darkWebScanFrequency;
|
|
|
|
return {
|
|
subscriptionId,
|
|
tier: subscription.tier,
|
|
frequency,
|
|
cron: CRON_EXPRESSIONS[frequency],
|
|
nextRun: frequency === 'realtime' ? 'event-driven' : CRON_EXPRESSIONS[frequency],
|
|
};
|
|
}
|
|
}
|
|
|
|
export const schedulerService = new SchedulerService();
|