205 lines
5.7 KiB
TypeScript
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)),
|
|
};
|
|
}
|