diff --git a/app/api/license/check/__tests__/route.test.ts b/app/api/license/check/__tests__/route.test.ts index ec1c1c9..d6413dd 100644 --- a/app/api/license/check/__tests__/route.test.ts +++ b/app/api/license/check/__tests__/route.test.ts @@ -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() + }) }) diff --git a/app/api/license/check/route.ts b/app/api/license/check/route.ts index 8d925bd..6633d88 100644 --- a/app/api/license/check/route.ts +++ b/app/api/license/check/route.ts @@ -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 }); } diff --git a/app/api/license/heartbeat/route.ts b/app/api/license/heartbeat/route.ts index 349fb0b..cf8f06f 100644 --- a/app/api/license/heartbeat/route.ts +++ b/app/api/license/heartbeat/route.ts @@ -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); } diff --git a/app/api/license/info/__tests__/route.test.ts b/app/api/license/info/__tests__/route.test.ts index 5df51ed..4a29a00 100644 --- a/app/api/license/info/__tests__/route.test.ts +++ b/app/api/license/info/__tests__/route.test.ts @@ -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 () => { diff --git a/app/api/license/info/route.ts b/app/api/license/info/route.ts index fc7f771..e190d0b 100644 --- a/app/api/license/info/route.ts +++ b/app/api/license/info/route.ts @@ -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, diff --git a/app/api/license/verify/route.ts b/app/api/license/verify/route.ts index 0f33a6f..55d2f57 100644 --- a/app/api/license/verify/route.ts +++ b/app/api/license/verify/route.ts @@ -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 }); } diff --git a/components/LicenseInfo.tsx b/components/LicenseInfo.tsx index cffcca9..d95ac6f 100644 --- a/components/LicenseInfo.tsx +++ b/components/LicenseInfo.tsx @@ -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 }; diff --git a/docs/商业化地基排查问题清单_20260524.md b/docs/商业化地基排查问题清单_20260524.md index da9dd53..3733bf7 100644 --- a/docs/商业化地基排查问题清单_20260524.md +++ b/docs/商业化地基排查问题清单_20260524.md @@ -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 免费版激活保存空签名,后续读取疑似失败(已修复) diff --git a/lib/__tests__/license-service.test.ts b/lib/__tests__/license-service.test.ts index 68bc622..2d54cb9 100644 --- a/lib/__tests__/license-service.test.ts +++ b/lib/__tests__/license-service.test.ts @@ -95,6 +95,7 @@ function createSignedLicense(overrides: Record = {}) { 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 = {}) { 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).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('本地许可证验签收紧', () => { diff --git a/lib/__tests__/license-types.test.ts b/lib/__tests__/license-types.test.ts index 46f4143..4990d66 100644 --- a/lib/__tests__/license-types.test.ts +++ b/lib/__tests__/license-types.test.ts @@ -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'); }); }); }); diff --git a/lib/license/license-service.ts b/lib/license/license-service.ts index ad2cead..360b697 100644 --- a/lib/license/license-service.ts +++ b/lib/license/license-service.ts @@ -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 { // ============ 内部辅助函数 ============ export async function saveLicenseLocally(licenseKey: string, license: SignedLicense): Promise { + 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 const existing = await getLocalLicense(); if (!existing) return; + if (!verifyLicenseSignature(license.data, license.signature)) { + throw new Error('许可证签名验证失败'); + } + const currentConfig = extractLicenseConfig(license); const localData: LocalLicenseData = { diff --git a/lib/license/types.ts b/lib/license/types.ts index 44aec25..ae9d6cb 100644 --- a/lib/license/types.ts +++ b/lib/license/types.ts @@ -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 = { + FREE: 3, + STANDARD: 30, + PRO: 60, +}; + // 各版本的默认配置 -export const LICENSE_CONFIGS: Record> = { +export const LICENSE_CONFIGS: Record> = { FREE: { type: 'FREE', limits: { @@ -142,11 +150,21 @@ export const LICENSE_CONFIGS: Record = { + FREE: 3, + STANDARD: 30, + PRO: 60, +}; + // 各版本的默认配置 -export const LICENSE_CONFIGS: Record> = { +export const LICENSE_CONFIGS: Record> = { FREE: { type: 'FREE', limits: { @@ -143,11 +151,21 @@ export const LICENSE_CONFIGS: Record