#!/usr/bin/env node import { PrismaClient } from "@prisma/client"; const prisma = new PrismaClient(); const ONE_MINUTE_MS = 60_000; const ONE_DAY_MS = 24 * 60 * 60 * 1000; const now = new Date(); const consumedRetentionHours = getPositiveNumber(process.env.AUTH_TOKEN_CONSUMED_RETENTION_HOURS, 24); const staleLockMinutes = getPositiveNumber(process.env.CAMPAIGN_RETRY_STALE_LOCK_MINUTES, 120); const webhookRetentionDays = getPositiveNumber(process.env.WEBHOOK_EVENT_RETENTION_DAYS, 30); const auditRetentionDays = getPositiveNumber(process.env.AUDIT_LOG_RETENTION_DAYS, 365); function getPositiveNumber(raw, fallback) { const parsed = Number(raw); if (!Number.isFinite(parsed) || parsed <= 0) { return fallback; } return parsed; } function printLine(message) { console.log(`[ops-maintenance] ${message}`); } async function main() { try { const consumedCutoff = new Date(now.getTime() - consumedRetentionHours * 60 * 60 * 1000); const staleLockCutoff = new Date(now.getTime() - staleLockMinutes * ONE_MINUTE_MS); const webhookCutoff = new Date(now.getTime() - webhookRetentionDays * ONE_DAY_MS); const auditCutoff = new Date(now.getTime() - auditRetentionDays * ONE_DAY_MS); const expiredTokens = await prisma.authToken.deleteMany({ where: { expiresAt: { lte: now }, consumedAt: null } }); const consumedTokens = await prisma.authToken.deleteMany({ where: { consumedAt: { lte: consumedCutoff } } }); const recoveredLock = await prisma.backgroundJobState.updateMany({ where: { lockedUntil: { lte: staleLockCutoff }, lastRunStatus: "running" }, data: { lockedUntil: null, lastRunStatus: "stale", lastError: "Recovered stale lock from maintenance cleanup", lastFailureAt: now, lastRunCompletedAt: now, consecutiveFailures: { increment: 1 } } }); const removedWebhookEvents = await prisma.webhookEvent.deleteMany({ where: { createdAt: { lte: webhookCutoff }, processStatus: { not: "skipped" } } }); const removedAuditLogs = await prisma.auditLog.deleteMany({ where: { createdAt: { lte: auditCutoff } } }); printLine(`Expired auth tokens deleted: ${expiredTokens.count}`); printLine(`Consumed auth tokens deleted (older than ${consumedRetentionHours}h): ${consumedTokens.count}`); printLine(`Recovered stale background locks: ${recoveredLock.count}`); printLine(`Webhook events deleted (older than ${webhookRetentionDays}d): ${removedWebhookEvents.count}`); printLine(`Audit logs deleted (older than ${auditRetentionDays}d): ${removedAuditLogs.count}`); const sampleTenant = await prisma.tenant.findFirst({ select: { id: true } }); const shouldWriteAudit = Boolean(sampleTenant) && ( expiredTokens.count > 0 || consumedTokens.count > 0 || recoveredLock.count > 0 ); if (shouldWriteAudit) { const tenantId = sampleTenant.id; await prisma.auditLog.create({ data: { tenantId, actorUserId: null, entityType: "system", entityId: "maintenance", action: "ops_maintenance_cleanup", metadataJson: JSON.stringify({ expiredAuthTokens: expiredTokens.count, consumedAuthTokens: consumedTokens.count, recoveredLocks: recoveredLock.count, webhookEventsDeleted: removedWebhookEvents.count, auditLogsDeleted: removedAuditLogs.count, consumedRetentionHours, staleLockMinutes, webhookRetentionDays, auditRetentionDays }) } }); } printLine("maintenance completed"); } catch (error) { const message = error instanceof Error ? error.message : String(error); console.error(`[ops-maintenance] failed: ${message}`); process.exitCode = 1; } finally { await prisma.$disconnect(); } } await main();