diff --git a/server/license-middleware.js b/server/license-middleware.js index 9f0132b..8906676 100644 --- a/server/license-middleware.js +++ b/server/license-middleware.js @@ -25,15 +25,28 @@ console.log( let freeJobInFlight = false; // ── Online validation tunables ────────────────────────────────────────────── +// 30 min default scheduled cycle catches revocations / suspensions / +// expirations within at most half an hour. Bounded so a key revoked on +// Keysat doesn't sit unnoticed for hours on the customer's machine. const VALIDATE_INTERVAL_MS = parseInt( - process.env.RECAP_VALIDATE_INTERVAL_MS || String(6 * 60 * 60 * 1000), + process.env.RECAP_VALIDATE_INTERVAL_MS || String(30 * 60 * 1000), 10 ); const ACTIVATE_VALIDATE_TIMEOUT_MS = 8000; -// How often to re-read the license file (fast path for keys set via the -// StartOS action — the 6 h online cycle is too slow for that UX). +// 5 s file poll is the fast path for keys set via the StartOS "Set Recap +// License" action — the cost is one stat call per file every 5 s, which +// is negligible. const LICENSE_FILE_POLL_MS = parseInt( - process.env.RECAP_LICENSE_FILE_POLL_MS || "30000", + process.env.RECAP_LICENSE_FILE_POLL_MS || "5000", + 10 +); +// Opportunistic refresh: when /api/license-status is hit and the cached +// LIC was last validated more than this long ago, fire validateOnline in +// the background. The web UI hits license-status on every page load, so +// revocations get caught the next time anyone opens the app — usually +// well under the scheduled 30 min tick. +const OPPORTUNISTIC_REFRESH_THRESHOLD_MS = parseInt( + process.env.RECAP_OPPORTUNISTIC_REFRESH_MS || String(10 * 60 * 1000), 10 ); @@ -199,6 +212,21 @@ export function setupLicenseMiddleware(app) { // through unauthenticated. export function setupLicenseRoutes(app) { app.get("/api/license-status", (_req, res) => { + // Opportunistic refresh: if the cached state is more than + // OPPORTUNISTIC_REFRESH_THRESHOLD_MS old, fire a validateOnline in + // the background. Doesn't block the response — the next status hit + // (or the next browser refresh) sees the updated state. Caps the + // worst-case revocation-detection latency for active users at the + // threshold value (default 10 min). + if (LIC.state === "licensed") { + const lastValidated = LIC.lastValidatedAt + ? new Date(LIC.lastValidatedAt).getTime() + : 0; + const ageMs = Date.now() - lastValidated; + if (ageMs > OPPORTUNISTIC_REFRESH_THRESHOLD_MS) { + refreshLicenseOnline("opportunistic").catch(() => {}); + } + } res.json(license.publicView(LIC)); });