const crypto = require("node:crypto"); const fs = require("node:fs"); function requiredText(value, fieldName) { const text = String(value || "").trim(); if (!text) throw new Error(`${fieldName}不能为空`); return text; } function normalizeTimestamp(value, fieldName) { const timestamp = requiredText(value, fieldName); const match = /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2})$/.exec(timestamp); if (!match) throw new Error(`${fieldName}格式必须为 YYYY-MM-DD HH:MM`); const [, year, month, day, hour, minute] = match.map(Number); const parsed = new Date(year, month - 1, day, hour, minute); if (parsed.getFullYear() !== year || parsed.getMonth() !== month - 1 || parsed.getDate() !== day || parsed.getHours() !== hour || parsed.getMinutes() !== minute) { throw new Error(`${fieldName}不是有效日期时间`); } return timestamp; } function signLicense(payload, privateKeyPath) { const licenseId = payload.license_id || crypto.randomUUID(); const deviceId = requiredText(payload.device_id, "device_id"); const deviceParts = deviceId.split("/"); if (deviceParts.length !== 2 || deviceParts.some((part) => !/^[a-z0-9][a-z0-9_-]{1,63}$/.test(part))) { throw new Error("device_id 必须是 company-code/line-code 格式"); } const issued = normalizeTimestamp(payload.issued, "签发时间"); const expiry = normalizeTimestamp(payload.expiry, "到期时间"); if (expiry <= issued) throw new Error("到期时间必须晚于签发时间"); const normalized = { license_id: licenseId, customer: requiredText(payload.customer, "customer"), company_id: requiredText(payload.company_id, "company_id"), production_line_id: requiredText(payload.production_line_id, "production_line_id"), device_id: deviceId, issued, expiry, features: requiredText(payload.features || "*", "features") }; const payloadBase64 = Buffer.from(JSON.stringify(normalized)).toString("base64"); const privateKey = fs.readFileSync(privateKeyPath); const signature = crypto.sign("sha256", Buffer.from(payloadBase64), { key: privateKey, padding: crypto.constants.RSA_PKCS1_PSS_PADDING, saltLength: crypto.constants.RSA_PSS_SALTLEN_MAX_SIGN }); return { payload: normalized, content: `${payloadBase64}|${signature.toString("base64")}` }; } module.exports = { signLicense };