const cron = require("node-cron"); async function claimAndRevokeExpiredDelegations(tx, batchSize) { return tx.$queryRaw` WITH picked AS ( SELECT id FROM "delegation_grants" WHERE revoked_at IS NULL AND valid_to IS NOT NULL AND valid_to <= NOW() ORDER BY valid_to ASC LIMIT ${batchSize} FOR UPDATE SKIP LOCKED ) UPDATE "delegation_grants" AS dg SET revoked_at = NOW() FROM picked WHERE dg.id = picked.id AND dg.revoked_at IS NULL RETURNING dg.id, dg.principal_employee_id, dg.delegate_employee_id, dg.revoked_at `; } async function revokeExpiredDelegations(prisma, logger, options = {}) { const batchSize = Math.max(1, Number(options.batchSize ?? 200)); let total = 0; let round = 0; while (true) { round += 1; const revokedRows = await prisma.$transaction(async (tx) => { const claimed = await claimAndRevokeExpiredDelegations(tx, batchSize); if (!claimed.length) { return []; } const requestTime = Date.now(); await tx.auditLog.createMany({ data: claimed.map((item, index) => { const revokedAt = item.revoked_at ? new Date(item.revoked_at).getTime() : requestTime; return { request_id: `job:delegation-revoke:${revokedAt}:${item.id}:${index}`, actor_id: item.principal_employee_id, action: "DELEGATION_AUTO_REVOKED", target_type: "delegation_grant", target_id: item.id, ip_address: null, user_agent: "system/node-cron", extra: { principalEmployeeId: item.principal_employee_id, delegateEmployeeId: item.delegate_employee_id, }, }; }), }); return claimed; }); if (!revokedRows.length) { break; } total += revokedRows.length; logger.info( { round, claimed: revokedRows.length, updated: revokedRows.length, audited: revokedRows.length, }, "Expired delegations batch processed", ); } if (total > 0) { logger.info({ revoked: total }, "Expired delegations auto-revoked"); } return total; } function startDelegationRevokeJob(fastify) { let running = false; const task = cron.schedule(fastify.config.delegationRevokeCron, async () => { if (running) { return; } running = true; try { const revoked = await revokeExpiredDelegations(fastify.prisma, fastify.log); fastify.metrics?.jobRunsTotal?.inc?.({ job: "delegation.revoke-expired", driver: "cron", outcome: "success", }); if (revoked > 0) { fastify.metrics?.jobProcessedTotal?.inc?.( { job: "delegation.revoke-expired", driver: "cron", kind: "revoked", }, revoked, ); } } catch (error) { fastify.log.error({ err: error }, "Delegation revoke job failed"); fastify.metrics?.jobRunsTotal?.inc?.({ job: "delegation.revoke-expired", driver: "cron", outcome: "failure", }); } finally { running = false; } }); fastify.addHook("onClose", async () => { task.stop(); task.destroy(); }); fastify.log.info( { cron: fastify.config.delegationRevokeCron }, "Delegation revoke job scheduled", ); return task; } module.exports = { revokeExpiredDelegations, startDelegationRevokeJob, };