加入许可证在线租约校验

This commit is contained in:
WuYongJie 2026-05-26 15:10:35 +08:00
parent b267b36486
commit 07192cf779
13 changed files with 311 additions and 20 deletions

View File

@ -2,15 +2,17 @@ import { NextRequest } from 'next/server'
jest.mock('@/lib/license/license-service', () => ({
getLocalLicense: jest.fn(),
isLicenseConfigExpired: jest.fn(),
}))
const { getLocalLicense } = require('@/lib/license/license-service')
const { getLocalLicense, isLicenseConfigExpired } = require('@/lib/license/license-service')
import { GET } from '../route'
describe('GET /api/license/check', () => {
beforeEach(() => {
jest.clearAllMocks()
isLicenseConfigExpired.mockReturnValue(false)
})
test('本地许可证通过验签时应恢复激活状态并写入 cookie', async () => {
@ -30,6 +32,7 @@ describe('GET /api/license/check', () => {
intranetPenetration: false,
},
expiresAt: null,
leaseExpiresAt: new Date(Date.now() + 60_000).toISOString(),
},
})
@ -39,6 +42,7 @@ describe('GET /api/license/check', () => {
expect(response.status).toBe(200)
expect(data.activated).toBe(true)
expect(response.cookies.get('license_activated')?.value).toBe('true')
expect(isLicenseConfigExpired).toHaveBeenCalled()
})
test('本地许可证为空或验签失败时应返回未激活', async () => {
@ -51,4 +55,34 @@ describe('GET /api/license/check', () => {
expect(data.activated).toBe(false)
expect(response.cookies.get('license_activated')).toBeUndefined()
})
test('本地许可证在线租约过期时不应恢复激活 cookie', async () => {
isLicenseConfigExpired.mockReturnValue(true)
getLocalLicense.mockResolvedValue({
instanceId: 'test-instance-id',
licenseKey: 'license-key',
licenseData: 'encrypted-license',
lastVerified: new Date().toISOString(),
cachedConfig: {
type: 'FREE',
limits: { maxUsers: 10, maxMaterials: 100 },
features: {
customTemplates: false,
systemSettings: false,
departmentIsolation: false,
apiAccess: false,
intranetPenetration: false,
},
expiresAt: null,
leaseExpiresAt: new Date(Date.now() - 60_000).toISOString(),
},
})
const response = await GET(new NextRequest('http://localhost/api/license/check'))
const data = await response.json()
expect(response.status).toBe(200)
expect(data.activated).toBe(false)
expect(response.cookies.get('license_activated')).toBeUndefined()
})
})

View File

@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
import { getLocalLicense } from '@/lib/license/license-service';
import { getLocalLicense, isLicenseConfigExpired } from '@/lib/license/license-service';
/**
* 检查数据库中是否有已激活的许可证
@ -8,7 +8,7 @@ import { getLocalLicense } from '@/lib/license/license-service';
export async function GET(request: NextRequest) {
try {
const license = await getLocalLicense();
if (!license) {
if (!license || isLicenseConfigExpired(license.cachedConfig)) {
return NextResponse.json({ activated: false });
}

View File

@ -3,6 +3,7 @@ import { generateFingerprint } from '@/lib/license/fingerprint-service';
import { getLocalLicense, updateLocalLicense, clearLocalLicense } from '@/lib/license/license-service';
import { buildLicenseServerLicenseApiUrl } from '@/lib/license/server-config';
import { prisma } from '@/lib/prisma';
import { verifyLicenseSignature } from '@/lib/license/crypto-service';
import type { HeartbeatResponse } from '@/lib/license/types';
export async function POST(request: NextRequest) {
@ -47,6 +48,13 @@ export async function POST(request: NextRequest) {
const data: HeartbeatResponse = await response.json();
if (data.acknowledged && data.license) {
if (!verifyLicenseSignature(data.license.data, data.license.signature)) {
return NextResponse.json({
acknowledged: false,
error: '许可证签名验证失败',
});
}
// ✅ 在后端更新本地缓存
await updateLocalLicense(data.license);
}

View File

@ -38,6 +38,7 @@ const { getLicense } = require('@/lib/license')
const activeProLicense = {
type: 'PRO',
expiresAt: null,
leaseExpiresAt: '2026-07-25T00:00:00.000Z',
limits: {
maxUsers: 100,
maxMaterials: 1000,
@ -103,6 +104,7 @@ describe('GET /api/license/info', () => {
expect(data.licenseKey).toBe('FLNX****0001')
expect(data.usage.users.current).toBe(12)
expect(data.usage.materials.current).toBe(345)
expect(data.leaseExpiresAt).toBe('2026-07-25T00:00:00.000Z')
})
it('最高权限账号可以看到完整激活码', async () => {

View File

@ -39,6 +39,7 @@ export async function GET() {
type: 'FREE' as const,
status: 'active' as const,
expiresAt: null,
leaseExpiresAt: null,
licenseKey: null,
usage: {
users: {
@ -81,7 +82,10 @@ export async function GET() {
// 判断状态
let status: 'active' | 'expired' | 'grace' = 'active';
if (license.expiresAt && new Date(license.expiresAt) < new Date()) {
if (
(license.expiresAt && new Date(license.expiresAt) < new Date()) ||
(license.leaseExpiresAt && new Date(license.leaseExpiresAt) < new Date())
) {
status = 'expired';
}
@ -89,6 +93,7 @@ export async function GET() {
type: license.type,
status,
expiresAt: license.expiresAt,
leaseExpiresAt: license.leaseExpiresAt,
usage: {
users: {
current: userCount,

View File

@ -1,6 +1,11 @@
import { NextRequest, NextResponse } from 'next/server';
import { generateFingerprint } from '@/lib/license/fingerprint-service';
import { updateLocalLicense, getLocalLicense, clearLocalLicense } from '@/lib/license/license-service';
import {
updateLocalLicense,
getLocalLicense,
clearLocalLicense,
isLicenseConfigExpired,
} from '@/lib/license/license-service';
import { verifyLicenseSignature } from '@/lib/license/crypto-service';
import { buildLicenseServerLicenseApiUrl } from '@/lib/license/server-config';
import type { VerifyResponse } from '@/lib/license/types';
@ -57,7 +62,7 @@ export async function POST(request: NextRequest) {
}
// 其他错误(如网络问题),检查宽限期
if (isWithinGracePeriod(localLicense.lastVerified)) {
if (!isLicenseConfigExpired(localLicense.cachedConfig) && isWithinGracePeriod(localLicense.lastVerified)) {
return NextResponse.json({ valid: true });
}
return NextResponse.json({ valid: false, error: data.error || '验证失败' });
@ -77,7 +82,11 @@ export async function POST(request: NextRequest) {
// 网络错误时检查宽限期
const localLicense = await getLocalLicense();
if (localLicense && isWithinGracePeriod(localLicense.lastVerified)) {
if (
localLicense &&
!isLicenseConfigExpired(localLicense.cachedConfig) &&
isWithinGracePeriod(localLicense.lastVerified)
) {
return NextResponse.json({ valid: true });
}

View File

@ -7,6 +7,7 @@ interface LicenseData {
type: 'FREE' | 'STANDARD' | 'PRO';
status: 'active' | 'expired' | 'grace';
expiresAt: string | null;
leaseExpiresAt: string | null;
licenseKey: string | null;
usage: {
users: { current: number; max: number };

View File

@ -387,24 +387,32 @@
客户影响:
许可证过期或降级后,某些功能可能因为本地缓存仍继续可用。商业化授权边界会变弱。当前已先在许可证底层检查入口收口:业务 API 调用的功能开关和数量限制每次都会读取本地已验签许可证,并检查是否过期,不再只相信当前进程里残留的旧缓存。
许可证过期、降级或在线租约过期后,某些功能可能因为本地缓存仍继续可用。商业化授权边界会变弱。当前已先在许可证底层检查入口收口:业务 API 调用的功能开关和数量限制每次都会读取本地已验签许可证,并检查许可证总到期和在线租约到期,不再只相信当前进程里残留的旧缓存。
证据:
- `lib/license/license-service.ts`:新增当前有效许可证读取逻辑,`isFeatureEnabled()`、`checkLimit()`、`getLicenseType()` 都会从本地已验签许可证重新取当前配置,并把过期许可证视为不可用。
- `lib/license/license-service.ts`:`saveLicenseLocally()`、`updateLocalLicense()` 会同步更新内存缓存;如果新许可证已过期,则不会继续保留可用缓存。
- `lib/__tests__/license-service.test.ts`:新增测试覆盖许可证过期后高级功能不可用、数量限制不放行,以及本地许可证从 Pro 降级到 Free 后业务检查立即按 Free 生效。
- `lib/license/types.ts`、`license-server/lib/types.ts`:签名许可证新增 `leaseExpiresAt` 在线租约字段;新发和续发许可证都会带租约。默认策略为免费版 3 天、标准版 30 天、专业版 60 天;如果许可证总到期早于默认租约,则租约会被截断到总到期时间。
- `lib/license/license-service.ts`:`saveLicenseLocally()`、`updateLocalLicense()` 保存前会再次验签,并同步更新内存缓存;如果新许可证已过期或在线租约已过期,则不会继续保留可用缓存。
- `lib/license/license-service.ts`、`app/api/license/verify/route.ts`:远端验证失败进入本地宽限期前,也会先检查签名许可证里的 `leaseExpiresAt`,避免租约已经过期还继续靠本地宽限期放行。
- `app/api/license/check/route.ts`:恢复 `license_activated` cookie 前会检查租约有效性,不能再靠一份过期本地许可证恢复激活状态。
- `app/api/license/heartbeat/route.ts`:心跳拿到验证服务器续发许可证后会先验签再写入本地缓存。
- `lib/__tests__/license-service.test.ts`:新增测试覆盖许可证过期、在线租约过期、高级功能不可用、数量限制不放行、本地许可证从 Pro 降级到 Free 后业务检查立即按 Free 生效,以及旧许可证缺少 `leaseExpiresAt` 时的兼容行为。
- `lib/__tests__/license-types.test.ts`、`app/api/license/check/__tests__/route.test.ts`:新增测试锁定默认租约天数、租约截断逻辑,以及租约过期时不恢复激活 cookie。
- `lib/__tests__/check-limits.test.ts`:功能检查 helper 已全部改为异步调用底层许可证检查。
验证:
- `npm test -- lib/__tests__/license-service.test.ts lib/__tests__/check-limits.test.ts --runInBand`
- `npm test -- lib/__tests__/license-types.test.ts lib/__tests__/license-service.test.ts app/api/license/check/__tests__/route.test.ts app/api/license/info/__tests__/route.test.ts --runInBand`
- `npx tsc --noEmit --project license-server/tsconfig.json`
- `npm run type-check`
建议动作:
- 已完成:受限业务 API 继续通过 `canUseCustomTemplates()`、`canUseDepartmentIsolation()`、`canUseSystemSettings()`、`canUploadMaterial()`、`canCreateUser()` 等 helper 检查;这些 helper 底层不再信旧缓存。
- 后续增强:吊销、宽限期、远端不可达时的产品策略仍需单独定规则;当前本轮先保证过期和降级不会因为本地旧缓存继续放行。
- 已完成:免费版采用更短在线租约,适合主要防破解和防白嫖;付费版采用更长租约,优先避免误伤正常客户,同时仍能限制付费凭证长期滥用。
- 后续增强:吊销、异常设备、时间回拨、验证服务器限流、模块包下载和付费核心文件拆分仍需单独推进;当前本轮先保证过期、降级和租约过期不会因为本地旧缓存继续放行。
### P1-05 免费版激活保存空签名,后续读取疑似失败(已修复)

View File

@ -95,6 +95,7 @@ function createSignedLicense(overrides: Record<string, any> = {}) {
intranetPenetration: false,
},
expiresAt: null,
leaseExpiresAt: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000).toISOString(),
issuedAt,
...overrides,
},
@ -122,6 +123,7 @@ function createStoredLocalLicense(overrides: Record<string, any> = {}) {
intranetPenetration: false,
},
expiresAt: null,
leaseExpiresAt: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000).toISOString(),
},
signedLicense: undefined,
signedLicenseData: undefined,
@ -256,6 +258,93 @@ describe('license-service', () => {
await expect(checkLimit('materials')).resolves.toEqual({ allowed: false, current: 0, max: 0 });
});
it('在线租约过期后应拒绝功能和额度使用', async () => {
const expiredLeaseAt = new Date(Date.now() - 60_000).toISOString();
(prisma.systemConfig.findUnique as jest.Mock).mockResolvedValue({
key: 'license',
value: JSON.stringify(createStoredLocalLicense({
signedLicenseData: {
type: 'PRO',
limits: { maxUsers: -1, maxMaterials: -1 },
features: {
customTemplates: true,
systemSettings: true,
departmentIsolation: true,
apiAccess: true,
intranetPenetration: true,
},
expiresAt: null,
leaseExpiresAt: expiredLeaseAt,
},
})),
});
const { isFeatureEnabled, getLicenseType, checkLimit } = await import('../license/license-service');
await expect(isFeatureEnabled('customTemplates')).resolves.toBe(false);
await expect(getLicenseType()).resolves.toBe('FREE');
await expect(checkLimit('users')).resolves.toEqual({ allowed: false, current: 0, max: 0 });
});
it('付费许可证租约未过期时应继续允许付费功能', async () => {
const futureLeaseAt = new Date(Date.now() + 60_000).toISOString();
(prisma.systemConfig.findUnique as jest.Mock).mockResolvedValue({
key: 'license',
value: JSON.stringify(createStoredLocalLicense({
signedLicenseData: {
type: 'PRO',
limits: { maxUsers: -1, maxMaterials: -1 },
features: {
customTemplates: true,
systemSettings: true,
departmentIsolation: true,
apiAccess: true,
intranetPenetration: true,
},
expiresAt: null,
leaseExpiresAt: futureLeaseAt,
},
})),
});
const { isFeatureEnabled, checkLimit } = await import('../license/license-service');
await expect(isFeatureEnabled('customTemplates')).resolves.toBe(true);
await expect(checkLimit('materials')).resolves.toEqual({ allowed: true, current: 0, max: -1 });
});
it('旧许可证没有在线租约字段时暂按兼容证处理', async () => {
const legacyLicense = createSignedLicense({
type: 'PRO',
limits: { maxUsers: -1, maxMaterials: -1 },
features: {
customTemplates: true,
systemSettings: true,
departmentIsolation: true,
apiAccess: true,
intranetPenetration: true,
},
expiresAt: null,
});
delete (legacyLicense.data as Record<string, unknown>).leaseExpiresAt;
(prisma.systemConfig.findUnique as jest.Mock).mockResolvedValue({
key: 'license',
value: JSON.stringify(createStoredLocalLicense({
signedLicense: legacyLicense,
})),
});
const { isFeatureEnabled, getLocalLicense } = await import('../license/license-service');
await expect(isFeatureEnabled('customTemplates')).resolves.toBe(true);
await expect(getLocalLicense()).resolves.toMatchObject({
cachedConfig: {
leaseExpiresAt: null,
},
});
});
it('业务功能检查每次以本地已验签许可证为准,降级后不继续信旧缓存', async () => {
const proLicense = createStoredLocalLicense({
signedLicenseData: {
@ -368,6 +457,36 @@ describe('license-service', () => {
expect(result.valid).toBe(false);
expect(result.error).toBe('未找到许可证');
});
it('在线租约过期后,网络失败也不应继续走本地宽限期', async () => {
const expiredLeaseAt = new Date(Date.now() - 60_000).toISOString();
(prisma.systemConfig.findUnique as jest.Mock).mockResolvedValue({
key: 'license',
value: JSON.stringify(createStoredLocalLicense({
lastVerified: new Date().toISOString(),
signedLicenseData: {
type: 'PRO',
limits: { maxUsers: -1, maxMaterials: -1 },
features: {
customTemplates: true,
systemSettings: true,
departmentIsolation: true,
apiAccess: true,
intranetPenetration: true,
},
expiresAt: null,
leaseExpiresAt: expiredLeaseAt,
},
})),
});
mockFetch.mockRejectedValueOnce(new Error('Network error'));
const { verify } = await import('../license/license-service');
const result = await verify();
expect(result.valid).toBe(false);
expect(result.error).toBe('网络错误');
});
});
describe('本地许可证验签收紧', () => {

View File

@ -7,6 +7,7 @@ import {
LicenseType,
LicenseConfig,
LICENSE_CONFIGS,
LICENSE_LEASE_DAYS,
generateLicenseConfig,
FeatureKey,
} from '../license/types';
@ -109,6 +110,7 @@ describe('generateLicenseConfig', () => {
expect(config.type).toBe('FREE');
expect(config.limits.maxUsers).toBe(10);
expect(config.limits.maxMaterials).toBe(100);
expect(config.leaseExpiresAt).toEqual(expect.any(String));
});
it('应该生成 STANDARD 版本配置', () => {
@ -148,6 +150,44 @@ describe('generateLicenseConfig', () => {
});
});
describe('在线租约处理', () => {
beforeEach(() => {
jest.useFakeTimers();
jest.setSystemTime(new Date('2026-05-26T00:00:00.000Z'));
});
afterEach(() => {
jest.useRealTimers();
});
it('应该按版本生成不同的默认在线租约', () => {
const freeConfig = generateLicenseConfig('FREE');
const standardConfig = generateLicenseConfig('STANDARD');
const proConfig = generateLicenseConfig('PRO');
expect(LICENSE_LEASE_DAYS.FREE).toBe(3);
expect(freeConfig.leaseExpiresAt).toBe('2026-05-29T00:00:00.000Z');
expect(standardConfig.leaseExpiresAt).toBe('2026-06-25T00:00:00.000Z');
expect(proConfig.leaseExpiresAt).toBe('2026-07-25T00:00:00.000Z');
});
it('许可证总到期早于默认租约时,应把租约截断到总到期时间', () => {
const expiresAt = new Date('2026-05-27T00:00:00.000Z');
const config = generateLicenseConfig('PRO', expiresAt);
expect(config.expiresAt).toBe('2026-05-27T00:00:00.000Z');
expect(config.leaseExpiresAt).toBe('2026-05-27T00:00:00.000Z');
});
it('传入显式租约时间时应使用该时间', () => {
const leaseExpiresAt = new Date('2026-06-01T12:00:00.000Z');
const config = generateLicenseConfig('STANDARD', null, leaseExpiresAt);
expect(config.expiresAt).toBeNull();
expect(config.leaseExpiresAt).toBe('2026-06-01T12:00:00.000Z');
});
});
describe('返回值结构', () => {
it('应该包含所有必需字段', () => {
const config = generateLicenseConfig('STANDARD', new Date());
@ -163,6 +203,7 @@ describe('generateLicenseConfig', () => {
expect(config).toHaveProperty('features.apiAccess');
expect(config).toHaveProperty('features.intranetPenetration');
expect(config).toHaveProperty('expiresAt');
expect(config).toHaveProperty('leaseExpiresAt');
});
it('返回的配置应该是新对象,不影响原始配置', () => {
@ -171,6 +212,7 @@ describe('generateLicenseConfig', () => {
expect(config1.expiresAt).not.toBe(config2.expiresAt);
expect(LICENSE_CONFIGS.FREE).not.toHaveProperty('expiresAt');
expect(LICENSE_CONFIGS.FREE).not.toHaveProperty('leaseExpiresAt');
});
});
});

View File

@ -24,13 +24,28 @@ let cachedLicense: LicenseConfig | null = null;
let lastVerified: Date | null = null;
let heartbeatTimer: NodeJS.Timeout | null = null;
function isLicenseConfigExpired(license: LicenseConfig | null): boolean {
return Boolean(license?.expiresAt && new Date(license.expiresAt) < new Date());
function isDateStringExpired(value: string | null | undefined): boolean {
if (!value) return false;
const expiresAt = new Date(value).getTime();
if (Number.isNaN(expiresAt)) return true;
return expiresAt < Date.now();
}
export function isLicenseConfigExpired(license: LicenseConfig | null): boolean {
return Boolean(
license &&
(isDateStringExpired(license.expiresAt) || isDateStringExpired(license.leaseExpiresAt))
);
}
function extractLicenseConfig(license: SignedLicense): LicenseConfig {
const { issuedAt: _issuedAt, ...config } = license.data;
return config;
return {
...config,
leaseExpiresAt: config.leaseExpiresAt ?? null,
};
}
function normalizeLocalLicenseData(localData: LocalLicenseData): LocalLicenseData | null {
@ -214,7 +229,7 @@ export async function verify(): Promise<{ valid: boolean; error?: string; revoke
}
// 其他错误,检查宽限期
if (isWithinGracePeriod(localLicense.lastVerified)) {
if (!isLicenseConfigExpired(localLicense.cachedConfig) && isWithinGracePeriod(localLicense.lastVerified)) {
cachedLicense = localLicense.cachedConfig;
return { valid: true };
}
@ -235,7 +250,11 @@ export async function verify(): Promise<{ valid: boolean; error?: string; revoke
// 网络错误时检查宽限期
const localLicense = await getLocalLicense();
if (localLicense && isWithinGracePeriod(localLicense.lastVerified)) {
if (
localLicense &&
!isLicenseConfigExpired(localLicense.cachedConfig) &&
isWithinGracePeriod(localLicense.lastVerified)
) {
cachedLicense = localLicense.cachedConfig;
return { valid: true };
}
@ -387,6 +406,10 @@ export async function initLicenseService(): Promise<void> {
// ============ 内部辅助函数 ============
export async function saveLicenseLocally(licenseKey: string, license: SignedLicense): Promise<void> {
if (!verifyLicenseSignature(license.data, license.signature)) {
throw new Error('许可证签名验证失败');
}
const instanceId = await getInstanceId();
const currentConfig = extractLicenseConfig(license);
@ -412,6 +435,10 @@ export async function updateLocalLicense(license: SignedLicense): Promise<void>
const existing = await getLocalLicense();
if (!existing) return;
if (!verifyLicenseSignature(license.data, license.signature)) {
throw new Error('许可证签名验证失败');
}
const currentConfig = extractLicenseConfig(license);
const localData: LocalLicenseData = {

View File

@ -16,6 +16,7 @@ export interface LicenseConfig {
intranetPenetration: boolean;
};
expiresAt: string | null; // ISO 日期字符串,null 表示永久
leaseExpiresAt: string | null; // 在线租约到期时间,旧许可证可为 null
}
// 签名后的许可证
@ -91,8 +92,15 @@ export interface LocalLicenseData {
// 功能键
export type FeatureKey = keyof LicenseConfig['features'];
// 各版本默认在线租约:免费版更短,付费版更宽松
export const LICENSE_LEASE_DAYS: Record<LicenseType, number> = {
FREE: 3,
STANDARD: 30,
PRO: 60,
};
// 各版本的默认配置
export const LICENSE_CONFIGS: Record<LicenseType, Omit<LicenseConfig, 'expiresAt'>> = {
export const LICENSE_CONFIGS: Record<LicenseType, Omit<LicenseConfig, 'expiresAt' | 'leaseExpiresAt'>> = {
FREE: {
type: 'FREE',
limits: {
@ -142,11 +150,21 @@ export const LICENSE_CONFIGS: Record<LicenseType, Omit<LicenseConfig, 'expiresAt
*/
export function generateLicenseConfig(
type: LicenseType,
expiresAt?: Date | null
expiresAt?: Date | null,
leaseExpiresAt?: Date
): LicenseConfig {
const baseConfig = LICENSE_CONFIGS[type];
const defaultLeaseExpiresAt = new Date(
Date.now() + LICENSE_LEASE_DAYS[type] * 24 * 60 * 60 * 1000
);
const resolvedLeaseExpiresAt = leaseExpiresAt || defaultLeaseExpiresAt;
const boundedLeaseExpiresAt = expiresAt && resolvedLeaseExpiresAt && expiresAt < resolvedLeaseExpiresAt
? expiresAt
: resolvedLeaseExpiresAt;
return {
...baseConfig,
expiresAt: expiresAt ? expiresAt.toISOString() : null,
leaseExpiresAt: boundedLeaseExpiresAt.toISOString(),
};
}

View File

@ -16,6 +16,7 @@ export interface LicenseConfig {
intranetPenetration: boolean;
};
expiresAt: string | null; // ISO 日期字符串,null 表示永久
leaseExpiresAt: string | null; // 在线租约到期时间,旧许可证可为 null
}
// 签名后的许可证
@ -92,8 +93,15 @@ export interface VersionCheckResponse {
downloadSize: string | null;
}
// 各版本默认在线租约:免费版更短,付费版更宽松
export const LICENSE_LEASE_DAYS: Record<LicenseType, number> = {
FREE: 3,
STANDARD: 30,
PRO: 60,
};
// 各版本的默认配置
export const LICENSE_CONFIGS: Record<LicenseType, Omit<LicenseConfig, 'expiresAt'>> = {
export const LICENSE_CONFIGS: Record<LicenseType, Omit<LicenseConfig, 'expiresAt' | 'leaseExpiresAt'>> = {
FREE: {
type: 'FREE',
limits: {
@ -143,11 +151,21 @@ export const LICENSE_CONFIGS: Record<LicenseType, Omit<LicenseConfig, 'expiresAt
*/
export function generateLicenseConfig(
type: LicenseType,
expiresAt?: Date | null
expiresAt?: Date | null,
leaseExpiresAt?: Date
): LicenseConfig {
const baseConfig = LICENSE_CONFIGS[type];
const defaultLeaseExpiresAt = new Date(
Date.now() + LICENSE_LEASE_DAYS[type] * 24 * 60 * 60 * 1000
);
const resolvedLeaseExpiresAt = leaseExpiresAt || defaultLeaseExpiresAt;
const boundedLeaseExpiresAt = expiresAt && resolvedLeaseExpiresAt && expiresAt < resolvedLeaseExpiresAt
? expiresAt
: resolvedLeaseExpiresAt;
return {
...baseConfig,
expiresAt: expiresAt ? expiresAt.toISOString() : null,
leaseExpiresAt: boundedLeaseExpiresAt.toISOString(),
};
}