initial relay scaffold
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
// 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 { getConfigSnapshot } from "./config.js";
|
||||
|
||||
const KEYSAT_CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
|
||||
|
||||
// Map<licenseKey, { result, validated_at }>
|
||||
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.
|
||||
let verifierLoaded = false;
|
||||
let verifier = null;
|
||||
async function loadVerifier() {
|
||||
if (verifierLoaded) return verifier;
|
||||
verifierLoaded = true;
|
||||
try {
|
||||
const mod = await import("@keysat/licensing-client");
|
||||
// Same pattern Recap uses: build a verifier with the embedded
|
||||
// public key. Recap's license.js shows the exact call; copy it
|
||||
// here. For v0.1 we only need offline verification — if the
|
||||
// vendor module signature differs across Recap versions we can
|
||||
// tweak this.
|
||||
if (mod?.createVerifier) {
|
||||
verifier = mod.createVerifier();
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
`[keysat] failed to load @keysat/licensing-client (${err.message}) — will treat all licenses as anonymous`
|
||||
);
|
||||
}
|
||||
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 <key>` 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(),
|
||||
}));
|
||||
}
|
||||
Reference in New Issue
Block a user