flnxi-Wu/lib/license/premium-module-service.ts
2026-05-28 15:11:36 +08:00

285 lines
9.5 KiB
TypeScript

import { prisma } from '@/lib/prisma';
import { decryptLocal, encryptLocal, hashData, verifySignature } from './crypto-service';
import { generateFingerprint } from './fingerprint-service';
import { buildSignedLicenseRequest } from './request-signature';
import { buildLicenseServerLicenseApiUrl, getLicenseProjectCode, getLicenseServerUrl } from './server-config';
import { getLocalLicense, isLicenseConfigExpired } from './license-service';
import type { LicenseType } from './types';
export interface PremiumModuleManifest {
version: number;
licenseType: Exclude<LicenseType, 'FREE'>;
modules: Array<{
id: string;
minLicenseType: Exclude<LicenseType, 'FREE'>;
sizeBytes: number;
sha256: string;
packageVersion?: number;
}>;
issuedAt: string;
expiresAt: string;
}
interface SignedPremiumModuleManifest {
success: boolean;
manifest?: PremiumModuleManifest;
signature?: string;
error?: string;
}
interface CachedPremiumModuleManifest {
manifest: PremiumModuleManifest;
signature: string;
syncedAt: string;
}
interface PremiumModulePackageResponse {
success: boolean;
package?: {
moduleId: string;
contentBase64: string;
sha256: string;
sizeBytes: number;
expiresAt: string;
};
error?: string;
}
interface CachedPremiumModulePackages {
packages: Record<
string,
{
contentBase64: string;
sha256: string;
sizeBytes: number;
expiresAt: string;
syncedAt: string;
}
>;
}
const PREMIUM_MODULE_CONFIG_KEY = 'premium_module_manifest';
const PREMIUM_MODULE_PACKAGES_CONFIG_KEY = 'premium_module_packages';
export const PREMIUM_MODULE_IDS = {
CUSTOM_TEMPLATES_CORE: 'custom-templates-core',
INTRANET_PENETRATION_CORE: 'intranet-penetration-core',
} as const;
function isPaidLicense(type: LicenseType): type is Exclude<LicenseType, 'FREE'> {
return type === 'STANDARD' || type === 'PRO';
}
function isManifestExpired(manifest: PremiumModuleManifest): boolean {
const expiresAt = new Date(manifest.expiresAt).getTime();
return Number.isNaN(expiresAt) || expiresAt <= Date.now();
}
function verifyPremiumModuleManifest(manifest: PremiumModuleManifest, signature: string): boolean {
return verifySignature(JSON.stringify(manifest), signature);
}
async function savePremiumModuleManifest(
manifest: PremiumModuleManifest,
signature: string
): Promise<void> {
const cached: CachedPremiumModuleManifest = {
manifest,
signature,
syncedAt: new Date().toISOString(),
};
await prisma.systemConfig.upsert({
where: { key: PREMIUM_MODULE_CONFIG_KEY },
update: { value: encryptLocal(JSON.stringify(cached)) },
create: { key: PREMIUM_MODULE_CONFIG_KEY, value: encryptLocal(JSON.stringify(cached)) },
});
}
async function getCachedPremiumModulePackages(): Promise<CachedPremiumModulePackages> {
try {
const config = await prisma.systemConfig.findUnique({
where: { key: PREMIUM_MODULE_PACKAGES_CONFIG_KEY },
});
if (!config) return { packages: {} };
const cached = JSON.parse(decryptLocal(config.value)) as CachedPremiumModulePackages;
if (!cached.packages || typeof cached.packages !== 'object') return { packages: {} };
return cached;
} catch (error) {
console.warn('[License] 读取付费模块包缓存失败:', error);
return { packages: {} };
}
}
async function savePremiumModulePackages(cached: CachedPremiumModulePackages): Promise<void> {
await prisma.systemConfig.upsert({
where: { key: PREMIUM_MODULE_PACKAGES_CONFIG_KEY },
update: { value: encryptLocal(JSON.stringify(cached)) },
create: { key: PREMIUM_MODULE_PACKAGES_CONFIG_KEY, value: encryptLocal(JSON.stringify(cached)) },
});
}
export async function getCachedPremiumModuleManifest(): Promise<CachedPremiumModuleManifest | null> {
try {
const config = await prisma.systemConfig.findUnique({
where: { key: PREMIUM_MODULE_CONFIG_KEY },
});
if (!config) return null;
const cached = JSON.parse(decryptLocal(config.value)) as CachedPremiumModuleManifest;
if (!cached.manifest || !cached.signature) return null;
if (isManifestExpired(cached.manifest)) return null;
if (!verifyPremiumModuleManifest(cached.manifest, cached.signature)) return null;
const localLicense = await getLocalLicense();
if (!localLicense || isLicenseConfigExpired(localLicense.cachedConfig)) return null;
if (!isPaidLicense(localLicense.cachedConfig.type)) return null;
if (cached.manifest.licenseType !== localLicense.cachedConfig.type) return null;
return cached;
} catch (error) {
console.warn('[License] 读取付费模块缓存失败:', error);
return null;
}
}
function buildPremiumModuleDownloadUrl(moduleId: string): string {
const baseUrl = getLicenseServerUrl();
const projectCode = encodeURIComponent(getLicenseProjectCode());
return `${baseUrl}/api/license/${projectCode}/premium-modules/${encodeURIComponent(moduleId)}`;
}
function isCachedPackageValid(
cached: CachedPremiumModulePackages,
module: PremiumModuleManifest['modules'][number]
): boolean {
const modulePackage = cached.packages[module.id];
if (!modulePackage) return false;
if (modulePackage.sha256 !== module.sha256) return false;
if (modulePackage.sizeBytes !== module.sizeBytes) return false;
if (hashData(modulePackage.contentBase64) !== module.sha256) return false;
const packageExpiresAt = new Date(modulePackage.expiresAt).getTime();
return !Number.isNaN(packageExpiresAt) && packageExpiresAt > Date.now();
}
async function downloadPremiumModulePackage(
module: PremiumModuleManifest['modules'][number],
fingerprint: Awaited<ReturnType<typeof generateFingerprint>>
): Promise<PremiumModulePackageResponse['package']> {
const signedRequest = buildSignedLicenseRequest('premium-module-download', {
moduleId: module.id,
instanceId: fingerprint.instanceId,
fingerprint,
});
const response = await fetch(buildPremiumModuleDownloadUrl(module.id), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(signedRequest),
});
const data = (await response.json()) as PremiumModulePackageResponse;
if (!response.ok || !data.success || !data.package) {
throw new Error(data.error || `付费模块 ${module.id} 下载失败`);
}
if (data.package.moduleId !== module.id) {
throw new Error(`付费模块 ${module.id} 返回内容不匹配`);
}
if (data.package.sha256 !== module.sha256 || hashData(data.package.contentBase64) !== module.sha256) {
throw new Error(`付费模块 ${module.id} 哈希校验失败`);
}
if (data.package.sizeBytes !== module.sizeBytes) {
throw new Error(`付费模块 ${module.id} 大小校验失败`);
}
return data.package;
}
export async function syncPremiumModules(): Promise<{ synced: boolean; skipped?: boolean; error?: string }> {
try {
const localLicense = await getLocalLicense();
if (!localLicense || isLicenseConfigExpired(localLicense.cachedConfig)) {
return { synced: false, skipped: true, error: '本地许可证不可用' };
}
if (!isPaidLicense(localLicense.cachedConfig.type)) {
return { synced: false, skipped: true, error: '免费版不需要付费模块' };
}
const fingerprint = await generateFingerprint();
const signedRequest = buildSignedLicenseRequest('premium-modules', {
instanceId: fingerprint.instanceId,
fingerprint,
});
const response = await fetch(buildLicenseServerLicenseApiUrl('premium-modules'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(signedRequest),
});
const data = (await response.json()) as SignedPremiumModuleManifest;
if (!response.ok || !data.success || !data.manifest || !data.signature) {
return {
synced: false,
error: data.error || '付费模块清单同步失败',
};
}
if (data.manifest.licenseType !== localLicense.cachedConfig.type) {
return { synced: false, error: '付费模块清单许可证类型不匹配' };
}
if (!verifyPremiumModuleManifest(data.manifest, data.signature)) {
return { synced: false, error: '付费模块清单签名验证失败' };
}
const cachedPackages = await getCachedPremiumModulePackages();
for (const moduleInfo of data.manifest.modules) {
if (isCachedPackageValid(cachedPackages, moduleInfo)) {
continue;
}
const modulePackage = await downloadPremiumModulePackage(moduleInfo, fingerprint);
if (!modulePackage) {
throw new Error(`付费模块 ${moduleInfo.id} 下载结果为空`);
}
cachedPackages.packages[moduleInfo.id] = {
contentBase64: modulePackage.contentBase64,
sha256: modulePackage.sha256,
sizeBytes: modulePackage.sizeBytes,
expiresAt: modulePackage.expiresAt,
syncedAt: new Date().toISOString(),
};
}
await savePremiumModuleManifest(data.manifest, data.signature);
await savePremiumModulePackages(cachedPackages);
return { synced: true };
} catch (error) {
const message = error instanceof Error ? error.message : '付费模块清单同步异常';
return { synced: false, error: message };
}
}
export async function hasPremiumModule(moduleId: string): Promise<boolean> {
const cached = await getCachedPremiumModuleManifest();
if (!cached) return false;
const moduleInfo = cached.manifest.modules.find((item) => item.id === moduleId);
if (!moduleInfo) return false;
const cachedPackages = await getCachedPremiumModulePackages();
return isCachedPackageValid(cachedPackages, moduleInfo);
}