/**
* 授权系统 H5(浏览器)客户端,无任何第三方依赖。
*
* 用法一:浏览器
* const client = new LicenseClient('https://license.example.com', 'A1B2C3D4E5F60718293A4B5C6D7E8F90');
* const login = await client.verify('FFCD-21F1-4349-EEE1-5411-7758', '办公电脑');
* if (login.status === 200 && login.data.ok) {
* // 保存 login.data.licenseTicket / expireUnix / remainDays
* }
*
* 用法二:ES Module
* import { LicenseClient, generateMachineCode } from './license-client.js';
*
* 机器码持久化(浏览器无法读取硬件信息):
* const machineCode = loadOrCreateMachineCode(); // 默认存 localStorage['license_machine_code']
*
* 离线验签(可选):请部署方提供 RSA 公钥(PEM),每次启动用 verifyTicket 校验本地票据。
*
* 注意:浏览器跨域请求需服务端返回 CORS 头;建议将授权服务与业务页同域部署,
* 或在服务端为 /api/card/* 配置允许的来源。
*/
(function (root, factory) {
if (typeof module === 'object' && module.exports) {
module.exports = factory();
} else if (typeof define === 'function' && define.amd) {
define([], factory);
} else {
root.LicenseClient = factory();
}
})(typeof self !== 'undefined' ? self : this, function () {
'use strict';
function b64ToBytes(b64) {
const normalized = b64.replace(/-/g, '+').replace(/_/g, '/');
const bin = atob(normalized);
const bytes = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
return bytes;
}
function bytesToB64(bytes) {
let bin = '';
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
return btoa(bin);
}
class ApiResponse {
constructor(status, body) {
this.status = status;
this.body = body;
try {
this.data = JSON.parse(body);
} catch (e) {
this.data = null;
}
}
}
class LicenseClient {
/**
* @param {string} baseUrl 服务端地址,如 https://license.example.com
* @param {string} machineCode 32 位机器码(自动转大写)
* @param {object} [options] { timeoutMs: 10000, appKey: '管理后台生成的软件接入密钥' }
*/
constructor(baseUrl, machineCode, options) {
this.baseUrl = String(baseUrl).replace(/\/+$/, '');
this.machineCode = String(machineCode || '').trim().toUpperCase();
this.appKey = String((options && options.appKey) || '').trim();
this.timeoutMs = (options && options.timeoutMs) || 10000;
this.fetchImpl = (options && options.fetchImpl) || (typeof fetch !== 'undefined' ? fetch.bind(globalThis) : null);
if (!this.fetchImpl) throw new Error('当前环境不支持 fetch,请提供 options.fetchImpl');
}
/** 附带接入密钥(启用后服务端据其校验卡密归属,拒绝跨空间、跨软件验证) */
_scope(payload) {
if (this.appKey) payload.appKey = this.appKey;
return payload;
}
/** 登录/验证:校验卡密并绑定当前设备,成功返回授权票据 */
verify(cardKey, deviceName) {
return this._post('/api/card/verify', this._scope({
cardKey,
machineCode: this.machineCode,
deviceName: deviceName || '',
}));
}
/** 心跳:按业务周期调用,维持设备在线状态并刷新到期时间 */
heartbeat(cardKey) {
return this._post('/api/card/heartbeat', this._scope({
cardKey,
machineCode: this.machineCode,
}));
}
/** 查询当前卡密已绑定的设备列表 */
listDevices(cardKey) {
return this._post('/api/card/devices', this._scope({
cardKey,
machineCode: this.machineCode,
}));
}
/** 解绑当前设备(换机或归还授权时调用) */
unbind(cardKey) {
return this._post('/api/card/unbind', this._scope({
cardKey,
machineCode: this.machineCode,
}));
}
/**
* 离线校验授权票据(可选,WebCrypto 需要 https 或 localhost 环境)。
* licenseTicket = base64(payload) + '.' + base64(rsa-sha256 签名),
* payload 含 ch(卡密哈希)/mc(机器码)/exp(到期 unix 秒)/ts(签发时间)。
*
* @param {string} ticket login.verify 返回的 licenseTicket
* @param {string} publicKeyPem 部署方提供的 RSA 公钥 PEM(-----BEGIN PUBLIC KEY-----)
* @returns {Promise