// Cached license validation against the Keysat license server. Two // layers: // 1. Offline signature verification using the vendored // @keysat/licensing-client. Fast and works without network. // 2. Cached online check against keysat.xyz (or wherever // relay_keysat_base_url points) — confirms the key hasn't been // revoked since the last sync. Cached per license-key for // KEYSAT_CACHE_TTL_MS to avoid hammering keysat on hot paths. // // Returns a normalized object with tier resolved from entitlements: // { state: "licensed" | "invalid" | "anonymous", // tier: "core" | "pro" | "max", // licenseUuid: string | null, // entitlements: string[], // reason: string | null } import fs from "fs"; import path from "path"; import { getConfigSnapshot } from "./config.js"; const KEYSAT_CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour // Map const cache = new Map(); // Dynamically import the licensing client so it doesn't block boot // if vendor/keysat-licensing-client is missing in dev environments. // Construct the verifier on first use with the issuer public key // embedded in the image at assets/issuer.pub — same PEM file Recap-app // ships, so a license that validates in Recap also validates here. // // IMPORTANT: this used to call `mod.createVerifier()` (no args), which // silently no-op'd because the licensing-client exports a `Verifier` // CLASS, not a `createVerifier` factory. The result was that the // verifier instance was always null → offline verification was skipped // → entitlements always came back empty → tierFromEntitlements() // always returned "core" regardless of what the license actually // contained. Symptom: a real Pro license activated cleanly in Recap // (showing the PRO badge) still saw Core credits on the relay. Fixed // by using the documented Verifier + PublicKey.fromPem(pem) shape. let verifierLoaded = false; let verifier = null; async function loadVerifier() { if (verifierLoaded) return verifier; verifierLoaded = true; try { const mod = await import("@keysat/licensing-client"); if (!mod?.Verifier || !mod?.PublicKey) { console.warn( "[keysat] @keysat/licensing-client missing Verifier/PublicKey exports — leaving verifier null (all licenses will resolve to Core)" ); return null; } // Locate the embedded issuer PEM. The Docker image copies // assets/ to /app/assets/, so when this module runs from // /app/server/keysat-client.js, the PEM is at ../assets/issuer.pub. // Allow KEYSAT_ISSUER_PEM_PATH env override for local dev / tests. const __dirname = path.dirname(new URL(import.meta.url).pathname); const pemPath = process.env.KEYSAT_ISSUER_PEM_PATH || path.join(__dirname, "..", "assets", "issuer.pub"); const pem = fs.readFileSync(pemPath, "utf8"); verifier = new mod.Verifier(mod.PublicKey.fromPem(pem)); console.log( `[keysat] verifier ready (issuer PEM loaded from ${pemPath})` ); } catch (err) { console.warn( `[keysat] failed to initialize verifier (${err.message}) — all licenses will resolve to Core until this is fixed` ); } return verifier; } // Resolve a tier from an entitlements set. Bundles aren't supported // yet — explicit entitlement names only. relay_max wins over relay_pro // if somehow both are present. function tierFromEntitlements(entitlements) { if (entitlements.has("relay_max")) return "max"; if (entitlements.has("relay_pro")) return "pro"; return "core"; } // Public entry: takes the raw `Authorization: Bearer ` value (or // null) and returns a resolved license. Anonymous = no header = Core // tier. export async function resolveLicense(rawAuth) { if (!rawAuth) { return { state: "anonymous", tier: "core", licenseUuid: null, entitlements: [], reason: null, }; } const key = stripBearer(rawAuth); if (!key) { return { state: "invalid", tier: "core", licenseUuid: null, entitlements: [], reason: "malformed_auth_header", }; } // Cache hit (still fresh)? const cached = cache.get(key); if (cached && Date.now() - cached.validated_at < KEYSAT_CACHE_TTL_MS) { return cached.result; } // Offline verify first — establishes the entitlements + license id. const v = await loadVerifier(); let offline = null; if (v) { try { offline = v.verify(key); } catch (err) { const result = { state: "invalid", tier: "core", licenseUuid: null, entitlements: [], reason: `verify_failed: ${err.message}`, }; cache.set(key, { result, validated_at: Date.now() }); return result; } } // If offline verify worked, use its payload as the source of truth // for entitlements + license id. Then hit keysat for revocation // status. const entitlements = new Set(offline?.payload?.entitlements || []); const licenseUuid = offline?.payload?.licenseUuid || null; let online = await onlineCheck(key); if (online && online.revoked) { const result = { state: "invalid", tier: "core", licenseUuid, entitlements: [], reason: online.reason || "revoked", }; cache.set(key, { result, validated_at: Date.now() }); return result; } const tier = tierFromEntitlements(entitlements); const result = { state: "licensed", tier, licenseUuid, entitlements: [...entitlements], reason: null, }; cache.set(key, { result, validated_at: Date.now() }); return result; } function stripBearer(raw) { const m = (raw || "").trim().match(/^Bearer\s+(.+)$/i); if (m) return m[1].trim(); // Accept a bare key without the Bearer prefix for tolerance. return raw.trim(); } // Best-effort online check. Returns null on network error (cache the // offline-verified result with a short TTL so we don't pound keysat // while it's down) or { revoked: boolean, reason?: string }. async function onlineCheck(licenseKey) { try { const cfg = await getConfigSnapshot(); const base = (cfg.relay_keysat_base_url || "").replace(/\/$/, ""); if (!base) return null; // POST /validate is the standard Keysat shape (per the licensing // client docs). If the actual endpoint differs we'll wire it up // once we point the relay at a real Keysat server. const res = await fetch(`${base}/validate`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ license_key: licenseKey }), signal: AbortSignal.timeout(5000), }); if (!res.ok) { console.warn(`[keysat] online check ${base}/validate returned ${res.status}`); return null; } const data = await res.json(); return { revoked: !!data?.revoked || data?.status === "revoked", reason: data?.reason || null, }; } catch (err) { console.warn(`[keysat] online check failed: ${err?.message}`); return null; } } export function snapshotCache() { return Array.from(cache.entries()).map(([k, v]) => ({ license_prefix: k.slice(0, 12) + "…", tier: v.result?.tier, state: v.result?.state, validated_at: new Date(v.validated_at).toISOString(), })); }