745 lines
25 KiB
TypeScript
745 lines
25 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: LicenseType;
|
|
instanceId: string;
|
|
modules: Array<{
|
|
id: string;
|
|
minLicenseType: LicenseType;
|
|
artifact: {
|
|
type: 'registry-image';
|
|
image: string;
|
|
digest?: string | null;
|
|
version: string;
|
|
};
|
|
sizeBytes: number;
|
|
sha256: string;
|
|
entitlementVersion?: number;
|
|
limits?: {
|
|
maxUsers: number;
|
|
maxMaterials: number;
|
|
};
|
|
}>;
|
|
issuedAt: string;
|
|
expiresAt: string;
|
|
}
|
|
|
|
interface SignedPremiumModuleManifest {
|
|
success: boolean;
|
|
manifest?: PremiumModuleManifest;
|
|
signature?: string;
|
|
error?: string;
|
|
}
|
|
|
|
interface CachedPremiumModuleManifest {
|
|
manifest: PremiumModuleManifest;
|
|
signature: string;
|
|
syncedAt: string;
|
|
}
|
|
|
|
interface PremiumModuleEntitlementResponse {
|
|
success: boolean;
|
|
entitlement?: {
|
|
moduleId: string;
|
|
entitlementBase64: string;
|
|
sha256: string;
|
|
sizeBytes: number;
|
|
expiresAt: string;
|
|
artifact: PremiumModuleManifest['modules'][number]['artifact'];
|
|
};
|
|
error?: string;
|
|
}
|
|
|
|
export interface PremiumModulePullCredential {
|
|
registryHost: string;
|
|
repository: string;
|
|
tag: string;
|
|
digest: string | null;
|
|
username: string;
|
|
pullToken: string;
|
|
expiresAt: string;
|
|
}
|
|
|
|
interface PremiumModulePullCredentialResponse {
|
|
success: boolean;
|
|
pullCredential?: PremiumModulePullCredential;
|
|
error?: string;
|
|
}
|
|
|
|
export type PremiumModuleRuntimeStatusValue =
|
|
| 'missing'
|
|
| 'pulling'
|
|
| 'running'
|
|
| 'unhealthy'
|
|
| 'expired'
|
|
| 'revoked';
|
|
|
|
export interface PremiumModuleRuntimeStatus {
|
|
moduleId: string;
|
|
image: string;
|
|
digest: string | null;
|
|
status: PremiumModuleRuntimeStatusValue;
|
|
lastSyncAt: string;
|
|
lastError?: string;
|
|
}
|
|
|
|
interface CachedPremiumModuleEntitlements {
|
|
entitlements: Record<
|
|
string,
|
|
{
|
|
entitlementBase64: string;
|
|
sha256: string;
|
|
sizeBytes: number;
|
|
expiresAt: string;
|
|
artifact: PremiumModuleManifest['modules'][number]['artifact'];
|
|
pullCredential?: PremiumModulePullCredential;
|
|
syncedAt: string;
|
|
}
|
|
>;
|
|
}
|
|
|
|
const PREMIUM_MODULE_CONFIG_KEY = 'premium_module_manifest';
|
|
const PREMIUM_MODULE_ENTITLEMENTS_CONFIG_KEY = 'premium_module_entitlements';
|
|
const PREMIUM_MODULE_RUNTIME_STATUS_CONFIG_KEY = 'premium_module_runtime_status';
|
|
const PREMIUM_MODULE_PULL_USERNAME = 'flnxi-module';
|
|
const PREMIUM_MODULE_HEALTH_TIMEOUT_MS = 3000;
|
|
|
|
export const PREMIUM_MODULE_IDS = {
|
|
LICENSE_LIMITS_CORE: 'license-limits-core',
|
|
CUSTOM_TEMPLATES_CORE: 'custom-templates-core',
|
|
INTRANET_PENETRATION_CORE: 'intranet-penetration-core',
|
|
} as const;
|
|
|
|
export interface AuthorizedUsageLimits {
|
|
maxUsers: number;
|
|
maxMaterials: number;
|
|
}
|
|
|
|
export interface PremiumModuleRuntimeGateResult {
|
|
available: boolean;
|
|
moduleId: string;
|
|
status?: PremiumModuleRuntimeStatusValue;
|
|
message?: string;
|
|
}
|
|
|
|
export interface PremiumModuleAuthorizeResult extends PremiumModuleRuntimeGateResult {
|
|
action: string;
|
|
}
|
|
|
|
type PremiumModuleActionContext = Record<string, unknown>;
|
|
|
|
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);
|
|
}
|
|
|
|
function isValidLimitValue(value: unknown): value is number {
|
|
return Number.isInteger(value) && (value as number) >= -1;
|
|
}
|
|
|
|
function isValidUsageLimits(limits: unknown): limits is AuthorizedUsageLimits {
|
|
if (!limits || typeof limits !== 'object') return false;
|
|
const value = limits as Partial<AuthorizedUsageLimits>;
|
|
return isValidLimitValue(value.maxUsers) && isValidLimitValue(value.maxMaterials);
|
|
}
|
|
|
|
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 getCachedPremiumModuleEntitlements(): Promise<CachedPremiumModuleEntitlements> {
|
|
try {
|
|
const config = await prisma.systemConfig.findUnique({
|
|
where: { key: PREMIUM_MODULE_ENTITLEMENTS_CONFIG_KEY },
|
|
});
|
|
|
|
if (!config) return { entitlements: {} };
|
|
|
|
const cached = JSON.parse(decryptLocal(config.value)) as CachedPremiumModuleEntitlements;
|
|
if (!cached.entitlements || typeof cached.entitlements !== 'object') return { entitlements: {} };
|
|
|
|
return cached;
|
|
} catch (error) {
|
|
console.warn('[License] 读取授权模块凭证缓存失败:', error);
|
|
return { entitlements: {} };
|
|
}
|
|
}
|
|
|
|
async function savePremiumModuleEntitlements(cached: CachedPremiumModuleEntitlements): Promise<void> {
|
|
await prisma.systemConfig.upsert({
|
|
where: { key: PREMIUM_MODULE_ENTITLEMENTS_CONFIG_KEY },
|
|
update: { value: encryptLocal(JSON.stringify(cached)) },
|
|
create: { key: PREMIUM_MODULE_ENTITLEMENTS_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 (cached.manifest.licenseType !== localLicense.cachedConfig.type) return null;
|
|
if (!cached.manifest.instanceId || cached.manifest.instanceId !== localLicense.instanceId) 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 buildPremiumModulePullTokenUrl(moduleId: string): string {
|
|
return `${buildPremiumModuleDownloadUrl(moduleId)}/pull-token`;
|
|
}
|
|
|
|
function parseModuleImage(image: string): { registryHost: string; repository: string; tag: string } | null {
|
|
const [registryHost, ...pathParts] = image.split('/');
|
|
if (!registryHost || pathParts.length === 0) return null;
|
|
|
|
const imagePath = pathParts.join('/');
|
|
const lastSlashIndex = imagePath.lastIndexOf('/');
|
|
const tagSeparatorIndex = imagePath.lastIndexOf(':');
|
|
if (tagSeparatorIndex <= lastSlashIndex) return null;
|
|
|
|
return {
|
|
registryHost,
|
|
repository: imagePath.slice(0, tagSeparatorIndex),
|
|
tag: imagePath.slice(tagSeparatorIndex + 1),
|
|
};
|
|
}
|
|
|
|
function isSameArtifact(
|
|
left: PremiumModuleManifest['modules'][number]['artifact'] | null | undefined,
|
|
right: PremiumModuleManifest['modules'][number]['artifact'] | null | undefined
|
|
): boolean {
|
|
if (!left || !right) return false;
|
|
return (
|
|
left.type === right.type &&
|
|
left.image === right.image &&
|
|
(left.digest || null) === (right.digest || null) &&
|
|
left.version === right.version
|
|
);
|
|
}
|
|
|
|
function isCachedEntitlementValid(
|
|
cached: CachedPremiumModuleEntitlements,
|
|
module: PremiumModuleManifest['modules'][number]
|
|
): boolean {
|
|
const entitlement = cached.entitlements[module.id];
|
|
if (!entitlement) return false;
|
|
if (!isSameArtifact(entitlement.artifact, module.artifact)) return false;
|
|
if (entitlement.sha256 !== module.sha256) return false;
|
|
if (entitlement.sizeBytes !== module.sizeBytes) return false;
|
|
if (hashData(entitlement.entitlementBase64) !== module.sha256) return false;
|
|
|
|
const entitlementExpiresAt = new Date(entitlement.expiresAt).getTime();
|
|
return !Number.isNaN(entitlementExpiresAt) && entitlementExpiresAt > Date.now();
|
|
}
|
|
|
|
function isPullCredentialValid(
|
|
credential: PremiumModulePullCredential | null | undefined,
|
|
module: PremiumModuleManifest['modules'][number]
|
|
): credential is PremiumModulePullCredential {
|
|
if (!credential) return false;
|
|
if (credential.username !== PREMIUM_MODULE_PULL_USERNAME) return false;
|
|
|
|
const imageParts = parseModuleImage(module.artifact.image);
|
|
if (!imageParts) return false;
|
|
|
|
if (credential.registryHost !== imageParts.registryHost) return false;
|
|
if (credential.repository !== imageParts.repository) return false;
|
|
if (credential.tag !== imageParts.tag) return false;
|
|
if ((credential.digest || null) !== (module.artifact.digest || null)) return false;
|
|
if (!credential.pullToken || typeof credential.pullToken !== 'string') return false;
|
|
|
|
const expiresAt = new Date(credential.expiresAt).getTime();
|
|
return !Number.isNaN(expiresAt) && expiresAt > Date.now();
|
|
}
|
|
|
|
function getPremiumModuleServiceBaseUrl(moduleId: string): string {
|
|
const envByModuleId: Record<string, string | undefined> = {
|
|
[PREMIUM_MODULE_IDS.LICENSE_LIMITS_CORE]: process.env.LICENSE_LIMITS_MODULE_URL,
|
|
[PREMIUM_MODULE_IDS.CUSTOM_TEMPLATES_CORE]: process.env.CUSTOM_TEMPLATES_MODULE_URL,
|
|
[PREMIUM_MODULE_IDS.INTRANET_PENETRATION_CORE]: process.env.INTRANET_PENETRATION_MODULE_URL,
|
|
};
|
|
|
|
return (envByModuleId[moduleId] || '').trim().replace(/\/+$/, '');
|
|
}
|
|
|
|
function isRuntimeStatusForModule(
|
|
status: PremiumModuleRuntimeStatus | null | undefined,
|
|
module: PremiumModuleManifest['modules'][number]
|
|
): boolean {
|
|
if (!status) return false;
|
|
if (status.moduleId !== module.id) return false;
|
|
if (status.status !== 'running') return false;
|
|
if (status.image !== module.artifact.image) return false;
|
|
if ((status.digest || null) !== (module.artifact.digest || null)) return false;
|
|
return true;
|
|
}
|
|
|
|
async function fetchModuleJson(
|
|
baseUrl: string,
|
|
path: string,
|
|
init: RequestInit = {}
|
|
): Promise<{ ok: boolean; status: number; data: any }> {
|
|
const controller = new AbortController();
|
|
const timer = setTimeout(() => controller.abort(), PREMIUM_MODULE_HEALTH_TIMEOUT_MS);
|
|
|
|
try {
|
|
const response = await fetch(`${baseUrl}${path}`, {
|
|
...init,
|
|
signal: controller.signal,
|
|
});
|
|
const data = await response.json().catch(() => null);
|
|
return { ok: response.ok, status: response.status, data };
|
|
} finally {
|
|
clearTimeout(timer);
|
|
}
|
|
}
|
|
|
|
async function checkPremiumModuleHealth(moduleId: string, baseUrl: string): Promise<boolean> {
|
|
try {
|
|
const result = await fetchModuleJson(baseUrl, '/health', { method: 'GET' });
|
|
return result.ok && result.data?.moduleId === moduleId && result.data?.status === 'healthy';
|
|
} catch (error) {
|
|
console.warn(`[License] 授权模块 ${moduleId} 健康检查失败:`, error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function getPremiumModuleInfo(
|
|
moduleId: string
|
|
): Promise<PremiumModuleManifest['modules'][number] | null> {
|
|
const cached = await getCachedPremiumModuleManifest();
|
|
if (!cached) return null;
|
|
return cached.manifest.modules.find((item) => item.id === moduleId) || null;
|
|
}
|
|
|
|
async function hasValidPremiumModuleEntitlement(
|
|
module: PremiumModuleManifest['modules'][number]
|
|
): Promise<boolean> {
|
|
const cachedEntitlements = await getCachedPremiumModuleEntitlements();
|
|
return isCachedEntitlementValid(cachedEntitlements, module);
|
|
}
|
|
|
|
async function downloadPremiumModuleEntitlement(
|
|
module: PremiumModuleManifest['modules'][number],
|
|
fingerprint: Awaited<ReturnType<typeof generateFingerprint>>,
|
|
manifestExpiresAt: string
|
|
): Promise<PremiumModuleEntitlementResponse['entitlement']> {
|
|
const signedRequest = buildSignedLicenseRequest('premium-module-download', {
|
|
moduleId: module.id,
|
|
instanceId: fingerprint.instanceId,
|
|
fingerprint,
|
|
manifestExpiresAt,
|
|
});
|
|
|
|
const response = await fetch(buildPremiumModuleDownloadUrl(module.id), {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(signedRequest),
|
|
});
|
|
const data = (await response.json()) as PremiumModuleEntitlementResponse;
|
|
|
|
if (!response.ok || !data.success || !data.entitlement) {
|
|
throw new Error(data.error || `授权模块 ${module.id} 授权凭证获取失败`);
|
|
}
|
|
|
|
if (data.entitlement.moduleId !== module.id) {
|
|
throw new Error(`授权模块 ${module.id} 返回内容不匹配`);
|
|
}
|
|
|
|
if (!isSameArtifact(data.entitlement.artifact, module.artifact)) {
|
|
throw new Error(`授权模块 ${module.id} 镜像地址校验失败`);
|
|
}
|
|
|
|
if (data.entitlement.sha256 !== module.sha256 || hashData(data.entitlement.entitlementBase64) !== module.sha256) {
|
|
throw new Error(`授权模块 ${module.id} 授权凭证哈希校验失败`);
|
|
}
|
|
|
|
if (data.entitlement.sizeBytes !== module.sizeBytes) {
|
|
throw new Error(`授权模块 ${module.id} 大小校验失败`);
|
|
}
|
|
|
|
return data.entitlement;
|
|
}
|
|
|
|
async function downloadPremiumModulePullCredential(
|
|
module: PremiumModuleManifest['modules'][number],
|
|
fingerprint: Awaited<ReturnType<typeof generateFingerprint>>
|
|
): Promise<PremiumModulePullCredential> {
|
|
const signedRequest = buildSignedLicenseRequest('premium-module-pull-token', {
|
|
moduleId: module.id,
|
|
instanceId: fingerprint.instanceId,
|
|
fingerprint,
|
|
});
|
|
|
|
const response = await fetch(buildPremiumModulePullTokenUrl(module.id), {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(signedRequest),
|
|
});
|
|
const data = (await response.json()) as PremiumModulePullCredentialResponse;
|
|
|
|
if (!response.ok || !data.success || !data.pullCredential) {
|
|
throw new Error(data.error || `授权模块 ${module.id} 镜像拉取凭证获取失败`);
|
|
}
|
|
|
|
if (!isPullCredentialValid(data.pullCredential, module)) {
|
|
throw new Error(`授权模块 ${module.id} 镜像拉取凭证校验失败`);
|
|
}
|
|
|
|
return data.pullCredential;
|
|
}
|
|
|
|
export async function getCachedPremiumModuleRuntimeStatus(): Promise<Record<string, PremiumModuleRuntimeStatus>> {
|
|
try {
|
|
const config = await prisma.systemConfig.findUnique({
|
|
where: { key: PREMIUM_MODULE_RUNTIME_STATUS_CONFIG_KEY },
|
|
});
|
|
|
|
if (!config) return {};
|
|
|
|
const cached = JSON.parse(decryptLocal(config.value)) as Record<string, PremiumModuleRuntimeStatus>;
|
|
if (!cached || typeof cached !== 'object') return {};
|
|
|
|
return cached;
|
|
} catch (error) {
|
|
console.warn('[License] 读取授权模块运行状态失败:', error);
|
|
return {};
|
|
}
|
|
}
|
|
|
|
async function savePremiumModuleRuntimeStatus(statuses: Record<string, PremiumModuleRuntimeStatus>): Promise<void> {
|
|
await prisma.systemConfig.upsert({
|
|
where: { key: PREMIUM_MODULE_RUNTIME_STATUS_CONFIG_KEY },
|
|
update: { value: encryptLocal(JSON.stringify(statuses)) },
|
|
create: { key: PREMIUM_MODULE_RUNTIME_STATUS_CONFIG_KEY, value: encryptLocal(JSON.stringify(statuses)) },
|
|
});
|
|
}
|
|
|
|
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: '本地许可证不可用' };
|
|
}
|
|
|
|
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 cachedEntitlements = await getCachedPremiumModuleEntitlements();
|
|
const runtimeStatuses = await getCachedPremiumModuleRuntimeStatus();
|
|
const activeModuleIds = new Set(data.manifest.modules.map((moduleInfo) => moduleInfo.id));
|
|
|
|
for (const moduleInfo of data.manifest.modules) {
|
|
const existing = cachedEntitlements.entitlements[moduleInfo.id];
|
|
const hasValidEntitlement = isCachedEntitlementValid(cachedEntitlements, moduleInfo);
|
|
let nextEntitlement = existing;
|
|
|
|
if (!hasValidEntitlement) {
|
|
const entitlement = await downloadPremiumModuleEntitlement(
|
|
moduleInfo,
|
|
fingerprint,
|
|
data.manifest.expiresAt
|
|
);
|
|
if (!entitlement) {
|
|
throw new Error(`授权模块 ${moduleInfo.id} 授权凭证为空`);
|
|
}
|
|
|
|
nextEntitlement = {
|
|
entitlementBase64: entitlement.entitlementBase64,
|
|
sha256: entitlement.sha256,
|
|
sizeBytes: entitlement.sizeBytes,
|
|
expiresAt: entitlement.expiresAt,
|
|
artifact: entitlement.artifact,
|
|
pullCredential: existing?.pullCredential,
|
|
syncedAt: new Date().toISOString(),
|
|
};
|
|
}
|
|
|
|
if (!nextEntitlement) {
|
|
throw new Error(`授权模块 ${moduleInfo.id} 本地凭证为空`);
|
|
}
|
|
|
|
if (!isPullCredentialValid(nextEntitlement.pullCredential, moduleInfo)) {
|
|
nextEntitlement.pullCredential = await downloadPremiumModulePullCredential(moduleInfo, fingerprint);
|
|
nextEntitlement.syncedAt = new Date().toISOString();
|
|
}
|
|
|
|
cachedEntitlements.entitlements[moduleInfo.id] = nextEntitlement;
|
|
|
|
const existingStatus = runtimeStatuses[moduleInfo.id];
|
|
const canCarryRuntimeStatus = Boolean(
|
|
existingStatus &&
|
|
existingStatus.image === moduleInfo.artifact.image &&
|
|
(existingStatus.digest || null) === (moduleInfo.artifact.digest || null) &&
|
|
existingStatus.status !== 'expired' &&
|
|
existingStatus.status !== 'revoked'
|
|
);
|
|
runtimeStatuses[moduleInfo.id] = {
|
|
moduleId: moduleInfo.id,
|
|
image: moduleInfo.artifact.image,
|
|
digest: moduleInfo.artifact.digest || null,
|
|
status: canCarryRuntimeStatus && existingStatus ? existingStatus.status : 'missing',
|
|
lastSyncAt: new Date().toISOString(),
|
|
lastError: canCarryRuntimeStatus && existingStatus ? existingStatus.lastError : undefined,
|
|
};
|
|
}
|
|
|
|
for (const [moduleId, status] of Object.entries(runtimeStatuses)) {
|
|
if (!activeModuleIds.has(moduleId)) {
|
|
runtimeStatuses[moduleId] = {
|
|
...status,
|
|
status: 'revoked',
|
|
lastSyncAt: new Date().toISOString(),
|
|
lastError: '许可证未包含该授权模块',
|
|
};
|
|
}
|
|
}
|
|
|
|
await savePremiumModuleManifest(data.manifest, data.signature);
|
|
await savePremiumModuleEntitlements(cachedEntitlements);
|
|
await savePremiumModuleRuntimeStatus(runtimeStatuses);
|
|
return { synced: true };
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : '授权模块清单同步异常';
|
|
return { synced: false, error: message };
|
|
}
|
|
}
|
|
|
|
export async function getAuthorizedUsageLimits(): Promise<AuthorizedUsageLimits | null> {
|
|
const moduleInfo = await getPremiumModuleInfo(PREMIUM_MODULE_IDS.LICENSE_LIMITS_CORE);
|
|
if (!moduleInfo || !isValidUsageLimits(moduleInfo.limits)) return null;
|
|
|
|
if (!(await hasValidPremiumModuleEntitlement(moduleInfo))) return null;
|
|
const authorization = await authorizePremiumModuleAction(
|
|
PREMIUM_MODULE_IDS.LICENSE_LIMITS_CORE,
|
|
'licenseLimits:enforce'
|
|
);
|
|
if (!authorization.available) return null;
|
|
|
|
return {
|
|
maxUsers: moduleInfo.limits.maxUsers,
|
|
maxMaterials: moduleInfo.limits.maxMaterials,
|
|
};
|
|
}
|
|
|
|
export async function getCachedModulePullCredential(moduleId: string): Promise<PremiumModulePullCredential | null> {
|
|
const cached = await getCachedPremiumModuleManifest();
|
|
if (!cached) return null;
|
|
|
|
const moduleInfo = cached.manifest.modules.find((item) => item.id === moduleId);
|
|
if (!moduleInfo) return null;
|
|
|
|
const cachedEntitlements = await getCachedPremiumModuleEntitlements();
|
|
if (!isCachedEntitlementValid(cachedEntitlements, moduleInfo)) return null;
|
|
|
|
const credential = cachedEntitlements.entitlements[moduleId]?.pullCredential;
|
|
return isPullCredentialValid(credential, moduleInfo) ? credential : null;
|
|
}
|
|
|
|
export async function hasPremiumModule(moduleId: string): Promise<boolean> {
|
|
const moduleInfo = await getPremiumModuleInfo(moduleId);
|
|
if (!moduleInfo) return false;
|
|
|
|
return hasValidPremiumModuleEntitlement(moduleInfo);
|
|
}
|
|
|
|
export async function isPremiumModuleRunning(moduleId: string): Promise<PremiumModuleRuntimeGateResult> {
|
|
const moduleInfo = await getPremiumModuleInfo(moduleId);
|
|
if (!moduleInfo) {
|
|
return {
|
|
available: false,
|
|
moduleId,
|
|
message: '当前许可证未包含该授权模块',
|
|
};
|
|
}
|
|
|
|
if (!(await hasValidPremiumModuleEntitlement(moduleInfo))) {
|
|
return {
|
|
available: false,
|
|
moduleId,
|
|
message: '授权模块凭证不可用,请重新验证许可证',
|
|
};
|
|
}
|
|
|
|
const runtimeStatuses = await getCachedPremiumModuleRuntimeStatus();
|
|
const runtimeStatus = runtimeStatuses[moduleId];
|
|
if (!isRuntimeStatusForModule(runtimeStatus, moduleInfo)) {
|
|
return {
|
|
available: false,
|
|
moduleId,
|
|
status: runtimeStatus?.status || 'missing',
|
|
message:
|
|
runtimeStatus?.lastError ||
|
|
'授权模块尚未运行,请先通过 Agent 拉取并启动对应模块',
|
|
};
|
|
}
|
|
|
|
const baseUrl = getPremiumModuleServiceBaseUrl(moduleId);
|
|
if (!baseUrl) {
|
|
return {
|
|
available: false,
|
|
moduleId,
|
|
status: runtimeStatus.status,
|
|
message: '授权模块服务地址未配置,无法确认模块健康状态',
|
|
};
|
|
}
|
|
|
|
if (!(await checkPremiumModuleHealth(moduleId, baseUrl))) {
|
|
return {
|
|
available: false,
|
|
moduleId,
|
|
status: 'unhealthy',
|
|
message: '授权模块健康检查未通过',
|
|
};
|
|
}
|
|
|
|
return {
|
|
available: true,
|
|
moduleId,
|
|
status: runtimeStatus.status,
|
|
};
|
|
}
|
|
|
|
export async function hasUsablePremiumModule(moduleId: string): Promise<boolean> {
|
|
return (await isPremiumModuleRunning(moduleId)).available;
|
|
}
|
|
|
|
export async function authorizePremiumModuleAction(
|
|
moduleId: string,
|
|
action: string,
|
|
context: PremiumModuleActionContext = {}
|
|
): Promise<PremiumModuleAuthorizeResult> {
|
|
const gate = await isPremiumModuleRunning(moduleId);
|
|
if (!gate.available) {
|
|
return {
|
|
...gate,
|
|
action,
|
|
};
|
|
}
|
|
|
|
const baseUrl = getPremiumModuleServiceBaseUrl(moduleId);
|
|
try {
|
|
const result = await fetchModuleJson(baseUrl, '/authorize', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ action, context }),
|
|
});
|
|
const allowed = result.ok && result.data?.allowed === true && result.data?.moduleId === moduleId;
|
|
return {
|
|
available: allowed,
|
|
moduleId,
|
|
action,
|
|
status: allowed ? 'running' : 'unhealthy',
|
|
message: allowed ? undefined : result.data?.error || '授权模块拒绝该操作',
|
|
};
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : '授权模块操作授权失败';
|
|
return {
|
|
available: false,
|
|
moduleId,
|
|
action,
|
|
status: 'unhealthy',
|
|
message,
|
|
};
|
|
}
|
|
}
|
|
|
|
export async function updatePremiumModuleRuntimeStatus(status: PremiumModuleRuntimeStatus): Promise<void> {
|
|
if (!status.moduleId || !status.image || !status.lastSyncAt) {
|
|
throw new Error('授权模块运行状态缺少必要字段');
|
|
}
|
|
|
|
const validStatuses: PremiumModuleRuntimeStatusValue[] = [
|
|
'missing',
|
|
'pulling',
|
|
'running',
|
|
'unhealthy',
|
|
'expired',
|
|
'revoked',
|
|
];
|
|
if (!validStatuses.includes(status.status)) {
|
|
throw new Error('授权模块运行状态无效');
|
|
}
|
|
|
|
const runtimeStatuses = await getCachedPremiumModuleRuntimeStatus();
|
|
runtimeStatuses[status.moduleId] = {
|
|
moduleId: status.moduleId,
|
|
image: status.image,
|
|
digest: status.digest || null,
|
|
status: status.status,
|
|
lastSyncAt: status.lastSyncAt,
|
|
lastError: status.lastError,
|
|
};
|
|
await savePremiumModuleRuntimeStatus(runtimeStatuses);
|
|
}
|