Add 'Set Recap License' StartOS action + s/Keysat license/Recap license/

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'.
This commit is contained in:
Keysat
2026-05-09 07:06:21 -05:00
parent 85cb641044
commit 29282f8dcc
6 changed files with 103 additions and 7 deletions
+18 -4
View File
@@ -40,6 +40,12 @@ export const LICENSE_PATH =
process.env.RECAP_LICENSE_KEY_PATH ||
path.join(DATA_DIR, "license.txt");
// StartOS config sidecar — the "Set Recap License" action writes the
// pasted key to this file's recap_license_key field. Read as a fallback
// after license.txt so the web-UI activation flow still wins if both
// are set.
const STARTOS_CONFIG_PATH = path.join(DATA_DIR, "config", "startos-config.json");
// Grace ceiling for network errors. As long as we successfully validated
// against Keysat within this window, we keep the license live even if
// subsequent online checks fail (Keysat down, customer offline, etc.).
@@ -73,15 +79,23 @@ function getOnlineClient() {
}
// ── Helpers ───────────────────────────────────────────────────────────────
// Resolution order:
// 1. RECAP_LICENSE_KEY env var (overrides everything; useful for tests)
// 2. license.txt at LICENSE_PATH (web-UI activation writes here)
// 3. recap_license_key in startos-config.json ("Set Recap License" action)
function readLicenseString() {
const fromEnv = (process.env.RECAP_LICENSE_KEY || "").trim();
if (fromEnv) return fromEnv;
try {
const s = fs.readFileSync(LICENSE_PATH, "utf8").trim();
return s || null;
} catch {
return null;
}
if (s) return s;
} catch {}
try {
const cfg = JSON.parse(fs.readFileSync(STARTOS_CONFIG_PATH, "utf8"));
const k = (cfg.recap_license_key || "").trim();
return k || null;
} catch {}
return null;
}
function readPersistedState() {