flnxi-Wu/license-server/lib/premium-module-manifest.ts
2026-06-02 19:25:48 +08:00

202 lines
5.8 KiB
TypeScript

import * as crypto from 'crypto';
import { signData } from './crypto';
import type { LicenseType } from './types';
import {
canLicenseUseModule,
getDefaultModuleGrantSpecs,
getModuleDefinition,
getModuleLimits,
getPremiumModuleArtifact,
type PremiumModuleArtifact,
} from './premium-module-catalog';
export const PREMIUM_MODULE_MANIFEST_VERSION = 1;
export const PREMIUM_MODULE_ENTITLEMENT_VERSION = 1;
export interface PremiumModuleManifest {
version: number;
licenseType: LicenseType;
instanceId: string;
modules: Array<{
id: string;
minLicenseType: LicenseType;
artifact: PremiumModuleArtifact;
sizeBytes: number;
sha256: string;
entitlementVersion: number;
limits?: {
maxUsers: number;
maxMaterials: number;
};
}>;
issuedAt: string;
expiresAt: string;
}
export interface SignedPremiumModuleManifest {
manifest: PremiumModuleManifest;
signature: string;
}
export interface PremiumModuleEntitlement {
moduleId: string;
entitlementBase64: string;
sha256: string;
sizeBytes: number;
expiresAt: string;
artifact: PremiumModuleArtifact;
}
export interface AuthorizedPremiumModuleGrant {
moduleId: string;
artifact: PremiumModuleArtifact;
limits?: {
maxUsers: number;
maxMaterials: number;
};
}
function getEntitlementSecret(): 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 encryptModuleEntitlement(payload: Record<string, unknown>): string {
const plaintext = JSON.stringify(payload);
const packageSecret = getEntitlementSecret();
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_ENTITLEMENT_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 buildPremiumModuleEntitlement(params: {
moduleId: string;
licenseType: LicenseType;
instanceId: string;
leaseExpiresAt: Date;
artifact?: PremiumModuleArtifact;
limits?: {
maxUsers: number;
maxMaterials: number;
};
authorizedByGrant?: boolean;
}): PremiumModuleEntitlement {
const module = getModuleDefinition(params.moduleId);
if (!module || (!params.authorizedByGrant && !canLicenseUseModule(params.licenseType, module))) {
throw new Error('授权模块不存在或许可证类型不允许使用');
}
const expiresAt = params.leaseExpiresAt.toISOString();
const artifact = params.artifact || getPremiumModuleArtifact(module.id);
const entitlementBase64 = encryptModuleEntitlement({
payloadType: 'flnxi-premium-module-entitlement',
entitlementVersion: PREMIUM_MODULE_ENTITLEMENT_VERSION,
moduleId: module.id,
licenseType: params.licenseType,
minLicenseType: module.minLicenseType,
capabilities: module.capabilities,
limits: params.limits ?? getModuleLimits(module.id, params.licenseType),
instanceId: params.instanceId,
artifact,
expiresAt,
});
return {
moduleId: module.id,
entitlementBase64,
sha256: crypto.createHash('sha256').update(entitlementBase64).digest('hex'),
sizeBytes: Buffer.byteLength(entitlementBase64, 'utf8'),
expiresAt,
artifact,
};
}
function buildModuleManifestItems(params: {
licenseType: LicenseType;
instanceId: string;
leaseExpiresAt: Date;
grants?: AuthorizedPremiumModuleGrant[];
}): PremiumModuleManifest['modules'] {
const grants = params.grants || getDefaultModuleGrantSpecs(params.licenseType);
return grants.map((grant) => {
const module = getModuleDefinition(grant.moduleId);
if (!module) {
throw new Error(`授权模块不存在:${grant.moduleId}`);
}
const entitlement = buildPremiumModuleEntitlement({
moduleId: grant.moduleId,
licenseType: params.licenseType,
instanceId: params.instanceId,
leaseExpiresAt: params.leaseExpiresAt,
artifact: grant.artifact,
limits: grant.limits,
authorizedByGrant: true,
});
return {
id: module.id,
minLicenseType: module.minLicenseType,
artifact: entitlement.artifact,
sizeBytes: entitlement.sizeBytes,
sha256: entitlement.sha256,
entitlementVersion: PREMIUM_MODULE_ENTITLEMENT_VERSION,
limits: grant.limits,
};
});
}
export function buildSignedPremiumModuleManifestForGrants(
licenseType: LicenseType,
leaseExpiresAt: Date,
instanceId: string,
grants?: AuthorizedPremiumModuleGrant[]
): SignedPremiumModuleManifest {
const manifest: PremiumModuleManifest = {
version: PREMIUM_MODULE_MANIFEST_VERSION,
licenseType,
instanceId,
modules: buildModuleManifestItems({ licenseType, instanceId, leaseExpiresAt, grants }),
issuedAt: new Date().toISOString(),
expiresAt: leaseExpiresAt.toISOString(),
};
return {
manifest,
signature: signData(JSON.stringify(manifest)),
};
}
export function buildSignedPremiumModuleManifest(
licenseType: LicenseType,
leaseExpiresAt: Date,
instanceId: string
): SignedPremiumModuleManifest {
return buildSignedPremiumModuleManifestForGrants(licenseType, leaseExpiresAt, instanceId);
}
// Backward-compatible export name for older internal tests/imports. This now
// returns an entitlement token, not the paid module core artifact itself.
export const buildPremiumModulePackage = buildPremiumModuleEntitlement;