Tighten license-poll cadence; add opportunistic online refresh

Three changes that together make license state changes feel
near-instant in the UI without burning real I/O / network budget:

  1. File-poll interval: 30s → 5s
     Action-set keys (via "Set Recap License") get picked up almost
     immediately. Cost: a single stat per file every 5s, negligible.

  2. Online validation interval: 6h → 30min
     A license revoked on Keysat now flips to invalid within 30 min
     worst-case, instead of sitting unnoticed for hours. Bounded
     latency makes revocation usable in production.

  3. Opportunistic online refresh on /api/license-status
     If the cached LIC was last validated more than 10 min ago, fire
     validateOnline() in the background (non-blocking) when the web
     UI hits the status endpoint. Since the UI hits status on every
     page load, revocations get caught the next time anyone opens
     the app — usually well under the scheduled 30 min tick.

Three new env vars for tuning:
  RECAP_LICENSE_FILE_POLL_MS         (default 5000)
  RECAP_VALIDATE_INTERVAL_MS         (default 1800000)
  RECAP_OPPORTUNISTIC_REFRESH_MS     (default 600000)
This commit is contained in:
Keysat
2026-05-09 19:36:46 -05:00
parent e5a779ced2
commit 9439154c25
+32 -4
View File
@@ -25,15 +25,28 @@ console.log(
let freeJobInFlight = false; let freeJobInFlight = false;
// ── Online validation tunables ────────────────────────────────────────────── // ── 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( 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 10
); );
const ACTIVATE_VALIDATE_TIMEOUT_MS = 8000; const ACTIVATE_VALIDATE_TIMEOUT_MS = 8000;
// How often to re-read the license file (fast path for keys set via the // 5 s file poll is the fast path for keys set via the StartOS "Set Recap
// StartOS action — the 6 h online cycle is too slow for that UX). // License" action — the cost is one stat call per file every 5 s, which
// is negligible.
const LICENSE_FILE_POLL_MS = parseInt( 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 10
); );
@@ -199,6 +212,21 @@ export function setupLicenseMiddleware(app) {
// through unauthenticated. // through unauthenticated.
export function setupLicenseRoutes(app) { export function setupLicenseRoutes(app) {
app.get("/api/license-status", (_req, res) => { 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)); res.json(license.publicView(LIC));
}); });