/** * License Guard — 本地授权校验核心 * * 三道门禁: * 1. Boot Gate: Gateway 启动时校验签名+过期 * 2. Feature Gate: 运行时检查功能开关 * 3. Quota Gate: 运行时检查配额 (Token/消息数) * * 零 SaaS 依赖,全部本地完成。 */ import * as crypto from 'crypto'; import * as fs from 'fs/promises'; import * as os from 'os'; import type { LicenseFile, LicensePayload, LicenseFeatures, LicenseQuotas, LicenseValidation, LicensePlan, } from './types.js'; import { PLAN_PRESETS } from './types.js'; // ========== 签名 ========== const SIGN_ALGORITHM = 'sha256'; /** * 对 payload 生成 HMAC-SHA256 签名 */ export function signPayload(payload: LicensePayload, secret: string): string { const data = JSON.stringify(payload, null, 0); return crypto.createHmac(SIGN_ALGORITHM, secret).update(data).digest('hex'); } /** * 校验签名是否匹配 */ export function verifySignature(payload: LicensePayload, signature: string, secret: string): boolean { const expected = signPayload(payload, secret); // 使用 timingSafeEqual 防止计时攻击 try { return crypto.timingSafeEqual( Buffer.from(expected, 'hex'), Buffer.from(signature, 'hex'), ); } catch { return false; } } // ========== 机器指纹 ========== /** * 生成简单机器指纹: hostname + cpuModel + totalMem * 不追求绝对唯一,只是防止 license 直接拷贝到别的机器 */ export function getMachineId(): string { const parts = [ os.hostname(), os.cpus()[0]?.model ?? 'unknown', String(os.totalmem()), os.platform(), os.arch(), ]; return crypto.createHash('sha256').update(parts.join('|')).digest('hex').substring(0, 16); } // ========== License Guard 类 ========== const GRACE_PERIOD_DAYS = 7; export class LicenseGuard { private license: LicensePayload | null = null; private validation: LicenseValidation | null = null; private usageFile: string; // 运行时用量 private dailyMessages = 0; private monthlyTokens = 0; private lastResetDay = 0; private lastResetMonth = 0; constructor( private licensePath: string, private secret: string, usagePath?: string, ) { this.usageFile = usagePath ?? licensePath.replace(/license\.json$/, 'usage.json'); } // ========== Boot Gate ========== /** * Gateway 启动时调用 — 读取 + 校验 License * @returns 校验结果,调用方根据 valid 决定是否继续启动 */ async boot(): Promise { try { const raw = await fs.readFile(this.licensePath, 'utf-8'); const file: LicenseFile = JSON.parse(raw); // 版本检查 if (file.version !== 1) { return this.fail('Unsupported license version: ' + file.version); } // 签名校验 if (!verifySignature(file.payload, file.signature, this.secret)) { return this.fail('Invalid license signature — file may be tampered'); } // 机器指纹校验 (如果设置了) if (file.payload.machineId) { const currentMachine = getMachineId(); if (file.payload.machineId !== currentMachine) { return this.fail( `License bound to different machine (expected: ${file.payload.machineId.substring(0, 8)}..., current: ${currentMachine.substring(0, 8)}...)`, ); } } // 过期检查 const now = Date.now(); const expiresAt = new Date(file.payload.expiresAt).getTime(); const daysRemaining = Math.ceil((expiresAt - now) / (24 * 60 * 60 * 1000)); const graceDeadline = expiresAt + GRACE_PERIOD_DAYS * 24 * 60 * 60 * 1000; if (now > graceDeadline) { return this.fail( `License expired on ${file.payload.expiresAt} — grace period (${GRACE_PERIOD_DAYS} days) also expired`, ); } const inGracePeriod = now > expiresAt; // 通过 ✅ this.license = file.payload; this.validation = { valid: true, payload: file.payload, daysRemaining: Math.max(daysRemaining, 0), inGracePeriod, }; // 加载运行时用量 await this.loadUsage(); if (inGracePeriod) { console.warn( `[License] ⚠️ License expired, in grace period (${GRACE_PERIOD_DAYS - Math.abs(daysRemaining)} days left). Plan degraded to FREE.`, ); } else if (daysRemaining <= 7) { console.warn(`[License] ⚠️ License expires in ${daysRemaining} days`); } else { console.log( `[License] ✅ ${file.payload.merchantName} | plan=${file.payload.plan} | expires=${file.payload.expiresAt} (${daysRemaining}d)`, ); } return this.validation; } catch (err: any) { if (err.code === 'ENOENT') { // 没有 license 文件 → 降级为 free console.log('[License] No license file found, running as FREE plan'); this.license = this.createFreeLicense(); this.validation = { valid: true, payload: this.license, daysRemaining: 365, inGracePeriod: false, }; return this.validation; } return this.fail('Failed to read license: ' + err.message); } } // ========== Feature Gate ========== /** * 检查某个功能是否启用 */ isFeatureEnabled(feature: keyof LicenseFeatures): boolean { if (!this.license) return false; // 宽限期内降级为 free if (this.validation?.inGracePeriod) { return PLAN_PRESETS.free.features[feature]; } return this.license.features[feature]; } /** * 获取当前生效的套餐 */ getEffectivePlan(): LicensePlan { if (!this.license) return 'free'; if (this.validation?.inGracePeriod) return 'free'; return this.license.plan; } /** * 获取当前配额 */ getQuotas(): LicenseQuotas { if (!this.license) return PLAN_PRESETS.free.quotas; if (this.validation?.inGracePeriod) return PLAN_PRESETS.free.quotas; return this.license.quotas; } // ========== Quota Gate ========== /** * 检查是否可以发送消息 (日消息配额) */ canSendMessage(): { allowed: boolean; reason?: string } { this.resetCountersIfNeeded(); const quotas = this.getQuotas(); if (quotas.dailyMessages === -1) return { allowed: true }; if (this.dailyMessages >= quotas.dailyMessages) { return { allowed: false, reason: `Daily message limit reached (${this.dailyMessages}/${quotas.dailyMessages})`, }; } return { allowed: true }; } /** * 检查是否可以消耗 Token (月 Token 配额) */ canConsumeTokens(estimatedTokens: number): { allowed: boolean; reason?: string } { this.resetCountersIfNeeded(); const quotas = this.getQuotas(); if (quotas.monthlyTokens === -1) return { allowed: true }; if (this.monthlyTokens + estimatedTokens > quotas.monthlyTokens) { return { allowed: false, reason: `Monthly token limit would be exceeded (${this.monthlyTokens}+${estimatedTokens}/${quotas.monthlyTokens})`, }; } return { allowed: true }; } /** * 记录消息发送 */ recordMessage(): void { this.resetCountersIfNeeded(); this.dailyMessages++; this.saveUsageDebounced(); } /** * 记录 Token 消耗 */ recordTokens(tokens: number): void { this.resetCountersIfNeeded(); this.monthlyTokens += tokens; this.saveUsageDebounced(); } /** * 获取用量摘要 */ getUsageSummary(): { dailyMessages: number; monthlyTokens: number; quotas: LicenseQuotas } { this.resetCountersIfNeeded(); return { dailyMessages: this.dailyMessages, monthlyTokens: this.monthlyTokens, quotas: this.getQuotas(), }; } // ========== 内部 ========== private fail(error: string): LicenseValidation { console.error(`[License] ❌ ${error}`); this.validation = { valid: false, error, daysRemaining: -1, inGracePeriod: false }; return this.validation; } private createFreeLicense(): LicensePayload { const now = new Date(); const expires = new Date(now); expires.setFullYear(expires.getFullYear() + 1); return { id: 'free-default', merchantId: 'local', merchantName: 'Free Plan', plan: 'free', features: { ...PLAN_PRESETS.free.features }, quotas: { ...PLAN_PRESETS.free.quotas }, issuedAt: now.toISOString(), expiresAt: expires.toISOString(), }; } private resetCountersIfNeeded(): void { const now = new Date(); const day = now.getDate(); const month = now.getMonth(); if (day !== this.lastResetDay) { this.dailyMessages = 0; this.lastResetDay = day; } if (month !== this.lastResetMonth) { this.monthlyTokens = 0; this.lastResetMonth = month; } } // 用量持久化 (防丢) private saveTimer: ReturnType | null = null; private saveUsageDebounced(): void { if (this.saveTimer) return; this.saveTimer = setTimeout(async () => { this.saveTimer = null; try { const data = { dailyMessages: this.dailyMessages, monthlyTokens: this.monthlyTokens, lastResetDay: this.lastResetDay, lastResetMonth: this.lastResetMonth, updatedAt: new Date().toISOString(), }; await fs.writeFile(this.usageFile, JSON.stringify(data, null, 2)); } catch { // 写失败不影响主流程 } }, 5000); // 5 秒防抖 } private async loadUsage(): Promise { try { const raw = await fs.readFile(this.usageFile, 'utf-8'); const data = JSON.parse(raw); this.dailyMessages = data.dailyMessages ?? 0; this.monthlyTokens = data.monthlyTokens ?? 0; this.lastResetDay = data.lastResetDay ?? new Date().getDate(); this.lastResetMonth = data.lastResetMonth ?? new Date().getMonth(); } catch { // 没有用量文件,从零开始 this.lastResetDay = new Date().getDate(); this.lastResetMonth = new Date().getMonth(); } } }