285 lines
9.5 KiB
TypeScript
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);
|
|
}
|