import schedule from "node-schedule"; import { db } from "../db/client.js"; import { secrets } from "../db/schema.js"; import { computeSecretStatus } from "./secretStatus.js"; import { notifySecretExpiry } from "./notify.js"; import { refreshTlsSecrets, type TlsCheckResult } from "./tlsCheck.js"; import { getSettings, getInternalFlag, setInternalFlag } from "./settingsStore.js"; const LAST_RUN_FLAG = "secretCheckLastRunDate"; /** * Refreshes every monitored certificate's expiry from the live server first, * so the alert below is computed from what's actually being served rather * than a stale date. This runs on every scheduled pass regardless of the * "secret expiry reminder" toggle — that toggle only controls whether a * notification is sent (notifySecretExpiry checks it itself). */ async function checkSecretExpiry(): Promise { let checkFailures: TlsCheckResult[] = []; try { checkFailures = (await refreshTlsSecrets()).filter((r) => !r.ok); } catch (err) { console.error("[secretExpiry] TLS refresh failed:", err); } const rows = await db.select().from(secrets); const expiring = rows .map((s) => ({ name: s.name, ...computeSecretStatus(s.expiryDate, s.warnDays) })) .filter((s): s is typeof s & { status: "expired" | "expiring" } => s.status === "expired" || s.status === "expiring"); await notifySecretExpiry(expiring, checkFailures); } async function checkSecretExpiryOnce(): Promise { const today = new Date().toDateString(); const lastRun = await getInternalFlag(LAST_RUN_FLAG); if (lastRun === today) return; await setInternalFlag(LAST_RUN_FLAG, today); await checkSecretExpiry(); } function cronFromTime(time: string): string { const [h, m] = time.split(":").map(Number); return `${Number.isFinite(m) ? m : 0} ${Number.isFinite(h) ? h : 8} * * *`; } let currentJob: schedule.Job | null = null; /** (Re)schedules the daily secret-expiry check per the current notification settings. Call again after settings change. */ export async function scheduleSecretExpiryCheck(): Promise { if (currentJob) { currentJob.cancel(); currentJob = null; } const { notifications } = await getSettings(); currentJob = schedule.scheduleJob({ rule: cronFromTime(notifications.secretCheckTime), tz: notifications.timezone }, () => { setInternalFlag(LAST_RUN_FLAG, "").catch(() => {}); checkSecretExpiry().catch((err) => console.error("[secretExpiry] check failed:", err)); }); console.log(`Secret expiry check scheduled at ${notifications.secretCheckTime} (${notifications.timezone})`); } /** Runs once at startup (skipped if already run today), then arms the daily schedule. */ export async function initSecretExpiryScheduler(): Promise { await checkSecretExpiryOnce(); await scheduleSecretExpiryCheck(); }