// ── Keysat license verification ────────────────────────────────────────── // // Reads a LIC1-... key from disk (or env), verifies its Ed25519 signature // against the operator's embedded public key, and exposes the resulting // state + entitlement set to the rest of the server. // // Operator config — keep these three constants in sync with what's set in // the Keysat admin UI: // ISSUER_PEM → assets/issuer.pub (committed; non-secret) // PRODUCT_SLUG → must match the product slug created in Keysat // KEYSAT_BASE_URL → optional, only used by online validate() / purchase // // Tier model for this app (see KEYSAT_INTEGRATION.md §0): // "core" — required for any business endpoint; unlocks // summarization and BYO Gemini API key // "history" — saved summary library: /api/history* // "library" — bulk import/export: /api/library/* // "subscriptions" — Pro: channel subs, auto-queue, sub-check log // "clips" — Pro: paperclip / clip-collection panel // // Tier policies: // Core → ["core", "history", "library"] // Pro → ["core", "history", "library", "subscriptions", "clips"] import fs from "fs"; import path from "path"; import { Verifier, PublicKey } from "@keysat/licensing-client"; export const PRODUCT_SLUG = "youtube-summarizer"; export const KEYSAT_BASE_URL = "https://licensing.keysat.xyz"; const __dirname = path.dirname(new URL(import.meta.url).pathname); const PEM_PATH = path.join(__dirname, "..", "assets", "issuer.pub"); const ISSUER_PEM = fs.readFileSync(PEM_PATH, "utf8"); // License file lives next to existing config/ and history/ in DATA_DIR. // On StartOS that's /data; on local Mac dev it's the project root. const DATA_DIR = process.env.DATA_DIR || path.join(__dirname, ".."); export const LICENSE_PATH = process.env.YT_SUMMARIZER_LICENSE_KEY_PATH || path.join(DATA_DIR, "license.txt"); // ── Verifier instance (built once at module load) ───────────────────────── let verifier = null; let verifierError = null; try { verifier = new Verifier(PublicKey.fromPem(ISSUER_PEM)); } catch (e) { verifierError = e?.message || String(e); console.error(`[license] failed to parse embedded public key: ${verifierError}`); } // ── Helpers ─────────────────────────────────────────────────────────────── function readLicenseString() { const fromEnv = (process.env.YT_SUMMARIZER_LICENSE_KEY || "").trim(); if (fromEnv) return fromEnv; try { const s = fs.readFileSync(LICENSE_PATH, "utf8").trim(); return s || null; } catch { return null; } } function emptyState(extra = {}) { return { state: "unlicensed", reason: null, licenseId: null, entitlements: new Set(), expiresAt: null, isTrial: false, ...extra, }; } // ── Public API ──────────────────────────────────────────────────────────── // // checkLicense() — read + verify; returns a frozen-ish state object. // Callers can re-invoke after activation to refresh. export function checkLicense() { if (verifierError) { return emptyState({ state: "invalid", reason: `bad embedded key: ${verifierError}` }); } const raw = readLicenseString(); if (!raw) return emptyState(); try { const ok = verifier.verify(raw); const payload = ok.payload || {}; // Reject keys minted for a different product (same operator, different SKU). if (payload.productSlug && payload.productSlug !== PRODUCT_SLUG) { return emptyState({ state: "invalid", reason: "product_mismatch" }); } return { state: "licensed", reason: null, licenseId: payload.licenseId || null, entitlements: new Set(payload.entitlements || []), expiresAt: payload.expiresAt ? new Date(payload.expiresAt * 1000) : null, isTrial: !!(payload.flags & 1), }; } catch (e) { return emptyState({ state: "invalid", reason: e?.message || "verify_failed" }); } } // activate(rawKey) — write a pasted key to disk, then re-check. // Returns the new license state. Throws on bad input format only; // signature failures surface as state: 'invalid' with a reason. export function activate(rawKey) { const key = (rawKey || "").trim(); if (!key.startsWith("LIC1-")) { const err = new Error("bad_format"); err.code = "bad_format"; throw err; } // Write atomically-ish: write to temp file then rename. const tmp = LICENSE_PATH + ".tmp"; fs.mkdirSync(path.dirname(LICENSE_PATH), { recursive: true }); fs.writeFileSync(tmp, key + "\n", { mode: 0o600 }); fs.renameSync(tmp, LICENSE_PATH); return checkLicense(); } // publicView(state) — safe shape for /api/license-status responses. // Never leaks the raw license key (it's a bearer credential). export function publicView(state) { return { state: state.state, reason: state.reason, licenseId: state.licenseId, entitlements: [...state.entitlements].sort(), expiresAt: state.expiresAt ? state.expiresAt.toISOString() : null, isTrial: !!state.isTrial, productSlug: PRODUCT_SLUG, keysatBaseUrl: KEYSAT_BASE_URL, licensePath: LICENSE_PATH, }; } // has(state, entitlement) — convenience wrapper for feature gates. export function has(state, entitlement) { return state && state.entitlements && state.entitlements.has(entitlement); }