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, }; }