Add period-based license keys alongside fixed expiry dates.

New keys default to validDays with countdown starting on first activation; existing expiresAtSec keys behave unchanged.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ivan Fontosh
2026-07-01 22:46:10 +08:00
parent e299cfc7dd
commit 4b568ceb56
7 changed files with 458 additions and 17 deletions
+35 -15
View File
@@ -4,6 +4,12 @@ import http from 'node:http';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import {
enrichLicenseEntry,
ensurePeriodActivated,
parseProductKeyExpiry,
resolveTokenExp,
} from '../lib/licenseLogic.mjs';
import { signPayload } from '../lib/signPayload.mjs';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -104,19 +110,37 @@ export function createServer(options = {}) {
if (!already && list.length >= pk.maxDevices) {
return json(res, 403, { error: 'too_many_devices' });
}
const now = Math.floor(Date.now() / 1000);
let dataChanged = false;
try {
const period = ensurePeriodActivated(pk, now);
if (period.changed) dataChanged = true;
} catch {
return json(res, 403, { error: 'license_misconfigured' });
}
if (!already) {
list.push(deviceId);
data.activations[pk.sub] = list;
writeData(data);
dataChanged = true;
}
if (dataChanged) writeData(data);
let exp;
try {
exp = resolveTokenExp(pk, now);
} catch {
return json(res, 403, { error: 'license_misconfigured' });
}
const now = Math.floor(Date.now() / 1000);
const payload = {
v: 1,
sub: pk.sub,
pid: pk.pid,
iat: now,
exp: pk.expiresAtSec,
exp,
did: deviceId,
};
const token = signPayload(payload, privateKeyPem);
@@ -131,16 +155,8 @@ export function createServer(options = {}) {
const activations = data.activations ?? {};
const licenses = (data.productKeys ?? []).map((pk) => {
const devices = activations[pk.sub] ?? [];
return {
key: pk.key,
sub: pk.sub,
pid: pk.pid,
maxDevices: pk.maxDevices,
expiresAtSec: pk.expiresAtSec,
revoked: revokedKeysList.includes(pk.key) || revokedSubsList.includes(pk.sub),
activatedDevices: devices,
activatedCount: devices.length,
};
const revoked = revokedKeysList.includes(pk.key) || revokedSubsList.includes(pk.sub);
return enrichLicenseEntry(pk, devices, revoked);
});
return json(res, 200, { licenses });
}
@@ -149,13 +165,17 @@ export function createServer(options = {}) {
if (!checkAdmin(req, adminToken)) return json(res, 401, { error: 'unauthorized' });
const raw = await readBody(req);
const body = JSON.parse(raw || '{}');
const now = Math.floor(Date.now() / 1000);
const expiry = parseProductKeyExpiry(body);
if (!expiry.ok) return json(res, 400, { error: expiry.error });
const entry = {
key: body.key ?? generateProductKey(),
sub: body.sub ?? generateSub(),
pid: body.pid ?? 'dnd_player',
maxDevices: body.maxDevices ?? 3,
expiresAtSec: body.expiresAtSec ?? now + 86400 * 365,
...(expiry.expiresAtSec != null
? { expiresAtSec: expiry.expiresAtSec }
: { validDays: expiry.validDays }),
};
if (!entry.key || !entry.sub) return json(res, 400, { error: 'invalid_entry' });
const data = readData();