flnxi-Wu/license-server/lib/premium-module-manifest.ts
2026-05-26 17:25:48 +08:00

205 lines
5.7 KiB
TypeScript

import * as crypto from 'crypto';
import { signData } from './crypto';
import type { LicenseType } from './types';
export const PREMIUM_MODULE_MANIFEST_VERSION = 1;
export const PREMIUM_MODULE_PACKAGE_VERSION = 1;
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;
}
export interface SignedPremiumModuleManifest {
manifest: PremiumModuleManifest;
signature: string;
}
export interface PremiumModulePackage {
moduleId: string;
contentBase64: string;
sha256: string;
sizeBytes: number;
expiresAt: string;
}
interface PremiumModuleDefinition {
id: string;
minLicenseType: Exclude<LicenseType, 'FREE'>;
capabilities: string[];
}
const MODULE_DEFINITIONS: Record<string, PremiumModuleDefinition> = {
'custom-templates-core': {
id: 'custom-templates-core',
minLicenseType: 'STANDARD',
capabilities: ['customTemplates'],
},
'intranet-penetration-core': {
id: 'intranet-penetration-core',
minLicenseType: 'PRO',
capabilities: ['intranetPenetration'],
},
};
const MODULE_IDS_BY_LICENSE: Record<Exclude<LicenseType, 'FREE'>, string[]> = {
STANDARD: ['custom-templates-core'],
PRO: ['custom-templates-core', 'intranet-penetration-core'],
};
function getPackageSecret(): string {
const secret = process.env.PREMIUM_MODULE_PACKAGE_SECRET || process.env.AES_SECRET_KEY || '';
if (!secret || secret.length < 32) {
throw new Error('PREMIUM_MODULE_PACKAGE_SECRET 或 AES_SECRET_KEY 未配置或长度不足');
}
return secret;
}
function canLicenseUseModule(
licenseType: Exclude<LicenseType, 'FREE'>,
module: PremiumModuleDefinition
): boolean {
if (module.minLicenseType === 'STANDARD') return licenseType === 'STANDARD' || licenseType === 'PRO';
return licenseType === 'PRO';
}
function encryptModulePayload(payload: Record<string, unknown>): string {
const plaintext = JSON.stringify(payload);
const packageSecret = getPackageSecret();
const key = crypto.createHash('sha256').update(packageSecret).digest();
const iv = crypto
.createHash('sha256')
.update(`${packageSecret}:${plaintext}`)
.digest()
.subarray(0, 12);
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
let encrypted = cipher.update(plaintext, 'utf8', 'base64');
encrypted += cipher.final('base64');
const envelope = {
version: PREMIUM_MODULE_PACKAGE_VERSION,
algorithm: 'aes-256-gcm',
iv: iv.toString('base64'),
authTag: cipher.getAuthTag().toString('base64'),
encrypted,
};
return Buffer.from(JSON.stringify(envelope), 'utf8').toString('base64');
}
export function buildPremiumModulePackage(params: {
moduleId: string;
licenseType: Exclude<LicenseType, 'FREE'>;
instanceId: string;
leaseExpiresAt: Date;
}): PremiumModulePackage {
const module = MODULE_DEFINITIONS[params.moduleId];
if (!module || !canLicenseUseModule(params.licenseType, module)) {
throw new Error('付费模块不存在或许可证类型不允许使用');
}
const expiresAt = params.leaseExpiresAt.toISOString();
const contentBase64 = encryptModulePayload({
packageType: 'flnxi-premium-module',
packageVersion: PREMIUM_MODULE_PACKAGE_VERSION,
moduleId: module.id,
licenseType: params.licenseType,
minLicenseType: module.minLicenseType,
capabilities: module.capabilities,
instanceId: params.instanceId,
expiresAt,
});
return {
moduleId: module.id,
contentBase64,
sha256: crypto.createHash('sha256').update(contentBase64).digest('hex'),
sizeBytes: Buffer.byteLength(contentBase64, 'utf8'),
expiresAt,
};
}
function buildModuleManifestItems(params: {
licenseType: Exclude<LicenseType, 'FREE'>;
instanceId: string;
leaseExpiresAt: Date;
}): PremiumModuleManifest['modules'] {
return MODULE_IDS_BY_LICENSE[params.licenseType].map((moduleId) => {
const module = MODULE_DEFINITIONS[moduleId];
const modulePackage = buildPremiumModulePackage({
moduleId,
licenseType: params.licenseType,
instanceId: params.instanceId,
leaseExpiresAt: params.leaseExpiresAt,
});
return {
id: module.id,
minLicenseType: module.minLicenseType,
sizeBytes: modulePackage.sizeBytes,
sha256: modulePackage.sha256,
packageVersion: PREMIUM_MODULE_PACKAGE_VERSION,
};
});
}
const MODULES_BY_LICENSE: Record<Exclude<LicenseType, 'FREE'>, PremiumModuleManifest['modules']> = {
STANDARD: [
{
id: 'custom-templates-core',
minLicenseType: 'STANDARD',
sizeBytes: 0,
sha256: '',
packageVersion: PREMIUM_MODULE_PACKAGE_VERSION,
},
],
PRO: [
{
id: 'custom-templates-core',
minLicenseType: 'STANDARD',
sizeBytes: 0,
sha256: '',
packageVersion: PREMIUM_MODULE_PACKAGE_VERSION,
},
{
id: 'intranet-penetration-core',
minLicenseType: 'PRO',
sizeBytes: 0,
sha256: '',
packageVersion: PREMIUM_MODULE_PACKAGE_VERSION,
},
],
};
export function buildSignedPremiumModuleManifest(
licenseType: Exclude<LicenseType, 'FREE'>,
leaseExpiresAt: Date,
instanceId?: string
): SignedPremiumModuleManifest {
const manifest: PremiumModuleManifest = {
version: PREMIUM_MODULE_MANIFEST_VERSION,
licenseType,
modules: instanceId
? buildModuleManifestItems({ licenseType, instanceId, leaseExpiresAt })
: MODULES_BY_LICENSE[licenseType],
issuedAt: new Date().toISOString(),
expiresAt: leaseExpiresAt.toISOString(),
};
return {
manifest,
signature: signData(JSON.stringify(manifest)),
};
}