import type { CronSchedule } from "./types.js"; const ONE_MINUTE_MS = 60 * 1000; const TEN_YEARS_MS = 10 * 365.25 * 24 * 60 * 60 * 1000; export type TimestampValidationError = { ok: false; message: string; }; export type TimestampValidationSuccess = { ok: true; }; export type TimestampValidationResult = TimestampValidationSuccess | TimestampValidationError; /** * Validates atMs timestamps in cron schedules. * Rejects timestamps that are: * - More than 1 minute in the past * - More than 10 years in the future */ export function validateScheduleTimestamp( schedule: CronSchedule, nowMs: number = Date.now(), ): TimestampValidationResult { if (schedule.kind !== "at") { return { ok: true }; } const atMs = schedule.atMs; if (typeof atMs !== "number" || !Number.isFinite(atMs)) { return { ok: false, message: `Invalid atMs: must be a finite number (got ${String(atMs)})`, }; } const diffMs = atMs - nowMs; // Check if timestamp is in the past (allow 1 minute grace period) if (diffMs < -ONE_MINUTE_MS) { const nowDate = new Date(nowMs).toISOString(); const atDate = new Date(atMs).toISOString(); const minutesAgo = Math.floor(-diffMs / ONE_MINUTE_MS); return { ok: false, message: `atMs is in the past: ${atDate} (${minutesAgo} minutes ago). Current time: ${nowDate}`, }; } // Check if timestamp is too far in the future if (diffMs > TEN_YEARS_MS) { const atDate = new Date(atMs).toISOString(); const yearsAhead = Math.floor(diffMs / (365.25 * 24 * 60 * 60 * 1000)); return { ok: false, message: `atMs is too far in the future: ${atDate} (${yearsAhead} years ahead). Maximum allowed: 10 years`, }; } return { ok: true }; }