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)
Two related changes:
1. New StartOS action: 'Set Recap License'
Symmetric with the existing 'Set Gemini API Key' action — paste a
LIC1-... key into the StartOS Actions menu and it gets persisted.
Added because some users prefer the StartOS form for credentials
over the in-app activation modal.
Implementation:
• startos/file-models/config.json.ts: schema gains recap_license_key
• startos/actions/setLicense.ts: input form (masked, regex-checks
for the LIC1- prefix), persists via configFile.merge()
• startos/actions/index.ts: registers the new action
• server/license.js: readLicenseString() falls back to
startos-config.json after the legacy license.txt path. Resolution
order: env → license.txt → startos-config.json
• server/license-middleware.js: faster license-file poll (30 s,
env-overridable RECAP_LICENSE_FILE_POLL_MS) re-runs checkLicense
so action-set keys take effect within seconds, not the 6 h online
cycle. If the new key parses as 'licensed', kicks an immediate
online check to confirm.
2. Copy fix: 'Keysat license' → 'Recap license' in user-facing text
Keysat is the licensing system underneath, but customers buy a
'Recap license'. Updated:
• Activation screen subtitle (public/index.html)
• 402 message in the activation gate (server/license-middleware.js)
Internal references (PRODUCT_SLUG, KEYSAT_BASE_URL, the issuer.pub
filename, the 'Issuer: licensing.keysat.xyz' display in the
activation card) stay as Keysat — those are accurate.
Smoke tested locally: starting the server with no license, then
writing a fake LIC1-... key into startos-config.json, the
license-file poll picks it up within ~2 s and transitions state from
'unlicensed' to 'invalid' (since the fake key fails Ed25519
verification, as expected). With a real key, the same path would land
in 'licensed'.
• LIC — exported live binding (ESM)
• setupLicenseMiddleware(app) — registers activation gate + Pro
feature gates (must run before any
/api/* route)
• setupLicenseRoutes(app) — /api/license-status, /api/license/
activate, /api/license/deactivate
• startLicenseRefresh() — startup + 6h periodic online check
• refreshLicenseOnline(reason) — ad-hoc refresh (e.g., during activate)
• isFreeUser() — 'no license || no core entitlement'
• tryAcquireFreeSlot() / releaseFreeSlot() — the free-tier concurrency
lock previously open-coded
in /api/process
Local 'const isFreeUser = ...' in /api/process renamed to 'isFree' to
avoid shadowing the imported helper. Open-coded freeJobInFlight reads/
writes replaced with the slot helpers.
server/index.js: 2461 → 2300 lines.
Smoke tested: server boots; /api/license-status, /api/health, /api/
process (rejects with 400 'No API key' as expected for unlicensed +
no key) all behave as before.