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:
@@ -0,0 +1,97 @@
|
||||
export const DEFAULT_VALID_DAYS = 365;
|
||||
export const SECONDS_PER_DAY = 86400;
|
||||
|
||||
export function isPeriodKey(pk) {
|
||||
return pk.validDays != null && pk.validDays > 0;
|
||||
}
|
||||
|
||||
export function isFixedKey(pk) {
|
||||
return pk.expiresAtSec != null && Number.isFinite(pk.expiresAtSec) && !isPeriodKey(pk);
|
||||
}
|
||||
|
||||
/** @returns {{ ok: true, expiresAtSec?: number, validDays?: number } | { ok: false, error: string }} */
|
||||
export function parseProductKeyExpiry(body, nowSec = Math.floor(Date.now() / 1000)) {
|
||||
const hasExpiresAtSec = body.expiresAtSec != null && body.expiresAtSec !== '';
|
||||
const hasValidDays = body.validDays != null && body.validDays !== '';
|
||||
|
||||
if (hasExpiresAtSec && hasValidDays) {
|
||||
return { ok: false, error: 'conflicting_expiry' };
|
||||
}
|
||||
|
||||
if (hasExpiresAtSec) {
|
||||
const expiresAtSec = Number(body.expiresAtSec);
|
||||
if (!Number.isFinite(expiresAtSec) || expiresAtSec <= 0) {
|
||||
return { ok: false, error: 'invalid_expiresAtSec' };
|
||||
}
|
||||
return { ok: true, expiresAtSec };
|
||||
}
|
||||
|
||||
if (hasValidDays) {
|
||||
const validDays = Number(body.validDays);
|
||||
if (!Number.isInteger(validDays) || validDays < 1) {
|
||||
return { ok: false, error: 'invalid_validDays' };
|
||||
}
|
||||
return { ok: true, validDays };
|
||||
}
|
||||
|
||||
return { ok: true, validDays: DEFAULT_VALID_DAYS };
|
||||
}
|
||||
|
||||
export function getExpiryMode(pk) {
|
||||
return isPeriodKey(pk) ? 'period' : 'fixed';
|
||||
}
|
||||
|
||||
export function getEffectiveExpSec(pk) {
|
||||
if (isFixedKey(pk)) return pk.expiresAtSec;
|
||||
if (isPeriodKey(pk) && pk.licenseExpSec != null) return pk.licenseExpSec;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* On first activation of a period key, sets activatedAtSec and licenseExpSec on pk.
|
||||
* @returns {{ changed: boolean, exp: number }}
|
||||
*/
|
||||
export function ensurePeriodActivated(pk, nowSec) {
|
||||
if (isFixedKey(pk)) {
|
||||
return { changed: false, exp: pk.expiresAtSec };
|
||||
}
|
||||
|
||||
if (!isPeriodKey(pk)) {
|
||||
throw new Error('invalid_product_key_expiry');
|
||||
}
|
||||
|
||||
if (pk.licenseExpSec != null && pk.activatedAtSec != null) {
|
||||
return { changed: false, exp: pk.licenseExpSec };
|
||||
}
|
||||
|
||||
pk.activatedAtSec = nowSec;
|
||||
pk.licenseExpSec = nowSec + pk.validDays * SECONDS_PER_DAY;
|
||||
return { changed: true, exp: pk.licenseExpSec };
|
||||
}
|
||||
|
||||
export function resolveTokenExp(pk, nowSec) {
|
||||
if (isFixedKey(pk)) return pk.expiresAtSec;
|
||||
if (isPeriodKey(pk)) {
|
||||
if (pk.licenseExpSec != null) return pk.licenseExpSec;
|
||||
return nowSec + pk.validDays * SECONDS_PER_DAY;
|
||||
}
|
||||
throw new Error('invalid_product_key_expiry');
|
||||
}
|
||||
|
||||
export function enrichLicenseEntry(pk, devices, revoked) {
|
||||
return {
|
||||
key: pk.key,
|
||||
sub: pk.sub,
|
||||
pid: pk.pid,
|
||||
maxDevices: pk.maxDevices,
|
||||
expiryMode: getExpiryMode(pk),
|
||||
expiresAtSec: pk.expiresAtSec ?? null,
|
||||
validDays: pk.validDays ?? null,
|
||||
activatedAtSec: pk.activatedAtSec ?? null,
|
||||
licenseExpSec: pk.licenseExpSec ?? null,
|
||||
effectiveExpSec: getEffectiveExpSec(pk),
|
||||
revoked,
|
||||
activatedDevices: devices,
|
||||
activatedCount: devices.length,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user