@nietzsci/clavis 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +127 -0
- package/dist/index.d.mts +291 -0
- package/dist/index.d.ts +291 -0
- package/dist/index.js +662 -0
- package/dist/index.mjs +617 -0
- package/examples/browser-example.html +87 -0
- package/examples/node-example.ts +67 -0
- package/package.json +28 -0
- package/src/client.ts +594 -0
- package/src/crypto.ts +84 -0
- package/src/errors.ts +46 -0
- package/src/fingerprint.ts +46 -0
- package/src/hmac.ts +67 -0
- package/src/index.ts +33 -0
- package/src/storage.ts +90 -0
- package/src/types.ts +114 -0
- package/tests/sdk.test.ts +548 -0
- package/tsconfig.json +19 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,662 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
Clavis: () => Clavis,
|
|
34
|
+
ClavisError: () => ClavisError,
|
|
35
|
+
ErrorCodes: () => ErrorCodes,
|
|
36
|
+
FileStorage: () => FileStorage,
|
|
37
|
+
default: () => index_default,
|
|
38
|
+
getDeviceInfo: () => getDeviceInfo,
|
|
39
|
+
getMachineCode: () => getMachineCode,
|
|
40
|
+
hashIdentity: () => hashIdentity,
|
|
41
|
+
signRequest: () => signRequest,
|
|
42
|
+
verifyOfflineToken: () => verifyOfflineToken
|
|
43
|
+
});
|
|
44
|
+
module.exports = __toCommonJS(index_exports);
|
|
45
|
+
|
|
46
|
+
// src/errors.ts
|
|
47
|
+
var ClavisError = class extends Error {
|
|
48
|
+
constructor(code, message) {
|
|
49
|
+
super(message);
|
|
50
|
+
this.name = "ClavisError";
|
|
51
|
+
this.code = code;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
var ErrorCodes = {
|
|
55
|
+
// 网络
|
|
56
|
+
NETWORK_ERROR: "NETWORK_ERROR",
|
|
57
|
+
API_ERROR: "API_ERROR",
|
|
58
|
+
// 签名
|
|
59
|
+
INVALID_SIGNATURE: "INVALID_SIGNATURE",
|
|
60
|
+
MISSING_AUTH_HEADERS: "MISSING_AUTH_HEADERS",
|
|
61
|
+
TIMESTAMP_EXPIRED: "TIMESTAMP_EXPIRED",
|
|
62
|
+
// 授权码
|
|
63
|
+
LICENSE_NOT_FOUND: "LICENSE_NOT_FOUND",
|
|
64
|
+
LICENSE_EXPIRED: "LICENSE_EXPIRED",
|
|
65
|
+
LICENSE_REVOKED: "LICENSE_REVOKED",
|
|
66
|
+
ACTIVATION_LIMIT: "ACTIVATION_LIMIT",
|
|
67
|
+
ACTIVATION_NOT_FOUND: "ACTIVATION_NOT_FOUND",
|
|
68
|
+
// 离线验证
|
|
69
|
+
NO_CACHED_TOKEN: "NO_CACHED_TOKEN",
|
|
70
|
+
TOKEN_SIGNATURE_INVALID: "TOKEN_SIGNATURE_INVALID",
|
|
71
|
+
TOKEN_EXPIRED: "TOKEN_EXPIRED",
|
|
72
|
+
TOKEN_GRACE_EXPIRED: "TOKEN_GRACE_EXPIRED",
|
|
73
|
+
IDENTITY_MISMATCH: "IDENTITY_MISMATCH",
|
|
74
|
+
CLOCK_DRIFT_DETECTED: "CLOCK_DRIFT_DETECTED",
|
|
75
|
+
// 配置
|
|
76
|
+
MISSING_CONFIG: "MISSING_CONFIG",
|
|
77
|
+
INVALID_CONFIG: "INVALID_CONFIG"
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// src/crypto.ts
|
|
81
|
+
var import_tweetnacl = __toESM(require("tweetnacl"));
|
|
82
|
+
var import_tweetnacl_util = require("tweetnacl-util");
|
|
83
|
+
function fromBase64Url(b64url) {
|
|
84
|
+
let b64 = b64url.replace(/-/g, "+").replace(/_/g, "/");
|
|
85
|
+
while (b64.length % 4 !== 0) b64 += "=";
|
|
86
|
+
return b64;
|
|
87
|
+
}
|
|
88
|
+
function verifyOfflineToken(token, publicKeyBase64) {
|
|
89
|
+
try {
|
|
90
|
+
const parts = token.split(".");
|
|
91
|
+
if (parts.length !== 2) return null;
|
|
92
|
+
const [payloadBase64Url, signatureBase64Url] = parts;
|
|
93
|
+
const payloadB64 = fromBase64Url(payloadBase64Url);
|
|
94
|
+
const payloadBytes = Uint8Array.from(
|
|
95
|
+
atob(payloadB64),
|
|
96
|
+
(c) => c.charCodeAt(0)
|
|
97
|
+
);
|
|
98
|
+
const payloadJson = new TextDecoder().decode(payloadBytes);
|
|
99
|
+
const signatureB64 = fromBase64Url(signatureBase64Url);
|
|
100
|
+
const signatureBytes = (0, import_tweetnacl_util.decodeBase64)(signatureB64);
|
|
101
|
+
const publicKeyBytes = (0, import_tweetnacl_util.decodeBase64)(publicKeyBase64);
|
|
102
|
+
const messageBytes = new TextEncoder().encode(payloadJson);
|
|
103
|
+
const signedMessage = new Uint8Array(
|
|
104
|
+
signatureBytes.length + messageBytes.length
|
|
105
|
+
);
|
|
106
|
+
signedMessage.set(signatureBytes);
|
|
107
|
+
signedMessage.set(messageBytes, signatureBytes.length);
|
|
108
|
+
const verified = import_tweetnacl.default.sign.open(signedMessage, publicKeyBytes);
|
|
109
|
+
if (!verified) return null;
|
|
110
|
+
return JSON.parse(payloadJson);
|
|
111
|
+
} catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// src/hmac.ts
|
|
117
|
+
var import_crypto = __toESM(require("crypto"));
|
|
118
|
+
function signRequest(params) {
|
|
119
|
+
const timestamp = Math.floor(Date.now() / 1e3).toString();
|
|
120
|
+
const nonce = import_crypto.default.randomUUID();
|
|
121
|
+
const bodyHash = import_crypto.default.createHash("sha256").update(params.body || "").digest("hex");
|
|
122
|
+
const stringToSign = `${params.method.toUpperCase()}
|
|
123
|
+
${params.path}
|
|
124
|
+
${timestamp}
|
|
125
|
+
${nonce}
|
|
126
|
+
${bodyHash}`;
|
|
127
|
+
const signature = import_crypto.default.createHmac("sha256", params.appSecret).update(stringToSign).digest("hex");
|
|
128
|
+
return {
|
|
129
|
+
"x-app-id": params.appId,
|
|
130
|
+
"x-timestamp": timestamp,
|
|
131
|
+
"x-nonce": nonce,
|
|
132
|
+
"x-signature": signature
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function hashIdentity(identity, appSecret) {
|
|
136
|
+
return import_crypto.default.createHmac("sha256", appSecret).update(identity).digest("hex");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// src/fingerprint.ts
|
|
140
|
+
var import_crypto2 = __toESM(require("crypto"));
|
|
141
|
+
var import_os = __toESM(require("os"));
|
|
142
|
+
function getMachineCode(appSecret) {
|
|
143
|
+
const cpus = import_os.default.cpus();
|
|
144
|
+
const data = [
|
|
145
|
+
import_os.default.hostname(),
|
|
146
|
+
import_os.default.platform(),
|
|
147
|
+
import_os.default.arch(),
|
|
148
|
+
cpus[0]?.model || "",
|
|
149
|
+
cpus.length.toString(),
|
|
150
|
+
import_os.default.totalmem().toString()
|
|
151
|
+
].join("|");
|
|
152
|
+
const key = appSecret || "clavis-default-key";
|
|
153
|
+
const hash = import_crypto2.default.createHmac("sha256", key).update(data).digest("hex");
|
|
154
|
+
return `MC-${hash.substring(0, 4).toUpperCase()}-${hash.substring(4, 8).toUpperCase()}-${hash.substring(8, 12).toUpperCase()}-${hash.substring(12, 16).toUpperCase()}`;
|
|
155
|
+
}
|
|
156
|
+
function getDeviceInfo() {
|
|
157
|
+
const cpus = import_os.default.cpus();
|
|
158
|
+
return {
|
|
159
|
+
os: import_os.default.platform(),
|
|
160
|
+
osVersion: import_os.default.release(),
|
|
161
|
+
hostname: import_os.default.hostname(),
|
|
162
|
+
arch: import_os.default.arch(),
|
|
163
|
+
cpuModel: cpus[0]?.model || "unknown",
|
|
164
|
+
totalMemory: import_os.default.totalmem()
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/storage.ts
|
|
169
|
+
var import_fs = __toESM(require("fs"));
|
|
170
|
+
var import_path = __toESM(require("path"));
|
|
171
|
+
var import_crypto3 = __toESM(require("crypto"));
|
|
172
|
+
var FileStorage = class {
|
|
173
|
+
constructor(storagePath, appId) {
|
|
174
|
+
const safeName = appId.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
175
|
+
this.filePath = import_path.default.join(storagePath, `.clavis_${safeName}`);
|
|
176
|
+
this.encryptionKey = import_crypto3.default.createHash("sha256").update(appId).digest();
|
|
177
|
+
}
|
|
178
|
+
/** 保存缓存数据(AES-256-GCM 加密) */
|
|
179
|
+
save(data) {
|
|
180
|
+
const json = JSON.stringify(data);
|
|
181
|
+
const iv = import_crypto3.default.randomBytes(12);
|
|
182
|
+
const cipher = import_crypto3.default.createCipheriv("aes-256-gcm", this.encryptionKey, iv);
|
|
183
|
+
const encrypted = Buffer.concat([
|
|
184
|
+
cipher.update(json, "utf8"),
|
|
185
|
+
cipher.final()
|
|
186
|
+
]);
|
|
187
|
+
const tag = cipher.getAuthTag();
|
|
188
|
+
const combined = Buffer.concat([iv, tag, encrypted]);
|
|
189
|
+
const dir = import_path.default.dirname(this.filePath);
|
|
190
|
+
if (!import_fs.default.existsSync(dir)) {
|
|
191
|
+
import_fs.default.mkdirSync(dir, { recursive: true });
|
|
192
|
+
}
|
|
193
|
+
import_fs.default.writeFileSync(this.filePath, combined);
|
|
194
|
+
}
|
|
195
|
+
/** 读取缓存数据(解密) */
|
|
196
|
+
load() {
|
|
197
|
+
try {
|
|
198
|
+
if (!import_fs.default.existsSync(this.filePath)) return null;
|
|
199
|
+
const data = import_fs.default.readFileSync(this.filePath);
|
|
200
|
+
if (data.length < 28) return null;
|
|
201
|
+
const iv = data.subarray(0, 12);
|
|
202
|
+
const tag = data.subarray(12, 28);
|
|
203
|
+
const encrypted = data.subarray(28);
|
|
204
|
+
const decipher = import_crypto3.default.createDecipheriv(
|
|
205
|
+
"aes-256-gcm",
|
|
206
|
+
this.encryptionKey,
|
|
207
|
+
iv
|
|
208
|
+
);
|
|
209
|
+
decipher.setAuthTag(tag);
|
|
210
|
+
const decrypted = decipher.update(encrypted, void 0, "utf8") + decipher.final("utf8");
|
|
211
|
+
return JSON.parse(decrypted);
|
|
212
|
+
} catch {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/** 清除缓存文件 */
|
|
217
|
+
clear() {
|
|
218
|
+
try {
|
|
219
|
+
if (import_fs.default.existsSync(this.filePath)) {
|
|
220
|
+
import_fs.default.unlinkSync(this.filePath);
|
|
221
|
+
}
|
|
222
|
+
} catch {
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
// src/client.ts
|
|
228
|
+
var Clavis = class {
|
|
229
|
+
constructor(config) {
|
|
230
|
+
this.statusListeners = [];
|
|
231
|
+
this.expiryListeners = [];
|
|
232
|
+
this.lastStatus = null;
|
|
233
|
+
this.currentLicenseKey = null;
|
|
234
|
+
this.currentIdentityHash = null;
|
|
235
|
+
if (!config.appId || !config.appSecret || !config.publicKey) {
|
|
236
|
+
throw new ClavisError(
|
|
237
|
+
ErrorCodes.MISSING_CONFIG,
|
|
238
|
+
"appId, appSecret, publicKey \u4E3A\u5FC5\u586B\u9879"
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
this.config = {
|
|
242
|
+
appId: config.appId,
|
|
243
|
+
appSecret: config.appSecret,
|
|
244
|
+
publicKey: config.publicKey,
|
|
245
|
+
baseUrl: (config.baseUrl || "https://c.nietzsci.com").replace(/\/+$/, ""),
|
|
246
|
+
storagePath: config.storagePath || process.cwd(),
|
|
247
|
+
offlineGraceDays: config.offlineGraceDays ?? 7,
|
|
248
|
+
heartbeatInterval: config.heartbeatInterval ?? 3600,
|
|
249
|
+
autoHeartbeat: config.autoHeartbeat ?? true
|
|
250
|
+
};
|
|
251
|
+
this.storage = new FileStorage(this.config.storagePath, this.config.appId);
|
|
252
|
+
}
|
|
253
|
+
// ===== 静态工具方法 =====
|
|
254
|
+
/** 生成设备机器码 */
|
|
255
|
+
static getMachineCode(appSecret) {
|
|
256
|
+
return getMachineCode(appSecret);
|
|
257
|
+
}
|
|
258
|
+
/** 获取设备信息 */
|
|
259
|
+
static getDeviceInfo() {
|
|
260
|
+
return getDeviceInfo();
|
|
261
|
+
}
|
|
262
|
+
// ===== 激活 =====
|
|
263
|
+
/**
|
|
264
|
+
* 激活授权码(需要联网)
|
|
265
|
+
*
|
|
266
|
+
* @param licenseKey 授权码,格式 XXXXX-XXXXX-XXXXX-XXXXX-XXXXX
|
|
267
|
+
* @param identity 设备标识(机器码或自定义字符串),SDK 会自动 HMAC 哈希
|
|
268
|
+
*/
|
|
269
|
+
async activate(licenseKey, identity) {
|
|
270
|
+
const identityHash = hashIdentity(identity, this.config.appSecret);
|
|
271
|
+
const body = JSON.stringify({
|
|
272
|
+
license_key: licenseKey,
|
|
273
|
+
identity_hash: identityHash
|
|
274
|
+
});
|
|
275
|
+
const data = await this.apiPost("/api/v1/sdk/activate", body);
|
|
276
|
+
this.storage.save({
|
|
277
|
+
offlineToken: data.offline_token,
|
|
278
|
+
lastVerifiedAt: Math.floor(Date.now() / 1e3),
|
|
279
|
+
tokenVersion: 0,
|
|
280
|
+
// 激活时不知道具体版本,从 token 解析
|
|
281
|
+
licenseKey
|
|
282
|
+
});
|
|
283
|
+
this.currentLicenseKey = licenseKey;
|
|
284
|
+
this.currentIdentityHash = identityHash;
|
|
285
|
+
if (this.config.autoHeartbeat) {
|
|
286
|
+
this.startHeartbeat(licenseKey, identity);
|
|
287
|
+
}
|
|
288
|
+
const result = {
|
|
289
|
+
success: true,
|
|
290
|
+
tier: data.tier,
|
|
291
|
+
features: data.features,
|
|
292
|
+
expiresAt: data.expires_at,
|
|
293
|
+
offlineToken: data.offline_token
|
|
294
|
+
};
|
|
295
|
+
this.notifyStatusChange(this.buildStatusFromActivate(result, false));
|
|
296
|
+
return result;
|
|
297
|
+
}
|
|
298
|
+
// ===== 验证 =====
|
|
299
|
+
/**
|
|
300
|
+
* 检查授权状态(优先离线,失败则在线)
|
|
301
|
+
*/
|
|
302
|
+
async checkLicense(identity) {
|
|
303
|
+
const offlineResult = this.verifyOffline(identity);
|
|
304
|
+
if (offlineResult.valid) return offlineResult;
|
|
305
|
+
const cached = this.storage.load();
|
|
306
|
+
if (!cached?.licenseKey) {
|
|
307
|
+
return offlineResult;
|
|
308
|
+
}
|
|
309
|
+
return this.verifyOnline(cached.licenseKey, identity);
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* 纯离线验证(零网络请求)
|
|
313
|
+
*
|
|
314
|
+
* @param identity 可选的设备标识,用于 identity_hash 匹配校验
|
|
315
|
+
*/
|
|
316
|
+
verifyOffline(identity) {
|
|
317
|
+
const invalid = {
|
|
318
|
+
valid: false,
|
|
319
|
+
tier: "",
|
|
320
|
+
features: [],
|
|
321
|
+
expiresAt: null,
|
|
322
|
+
daysRemaining: null,
|
|
323
|
+
isOffline: true,
|
|
324
|
+
isTrial: false,
|
|
325
|
+
inGracePeriod: false
|
|
326
|
+
};
|
|
327
|
+
const cached = this.storage.load();
|
|
328
|
+
if (!cached?.offlineToken) {
|
|
329
|
+
return { ...invalid, tier: "none" };
|
|
330
|
+
}
|
|
331
|
+
const payload = verifyOfflineToken(
|
|
332
|
+
cached.offlineToken,
|
|
333
|
+
this.config.publicKey
|
|
334
|
+
);
|
|
335
|
+
if (!payload) {
|
|
336
|
+
return { ...invalid, tier: "invalid_signature" };
|
|
337
|
+
}
|
|
338
|
+
if (payload.app_id !== this.config.appId) {
|
|
339
|
+
return { ...invalid, tier: "app_mismatch" };
|
|
340
|
+
}
|
|
341
|
+
if (identity) {
|
|
342
|
+
const expectedHash = hashIdentity(identity, this.config.appSecret);
|
|
343
|
+
if (payload.identity_hash !== expectedHash) {
|
|
344
|
+
return { ...invalid, tier: "identity_mismatch" };
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
348
|
+
if (cached.lastVerifiedAt > now + 60) {
|
|
349
|
+
return { ...invalid, tier: "clock_drift" };
|
|
350
|
+
}
|
|
351
|
+
const isExpired = payload.expires_at !== null && payload.expires_at < now;
|
|
352
|
+
const inGracePeriod = isExpired && payload.grace_until > now;
|
|
353
|
+
const graceExpired = isExpired && payload.grace_until <= now;
|
|
354
|
+
if (graceExpired) {
|
|
355
|
+
return {
|
|
356
|
+
...invalid,
|
|
357
|
+
tier: payload.tier,
|
|
358
|
+
features: payload.features,
|
|
359
|
+
expiresAt: payload.expires_at ? new Date(payload.expires_at * 1e3).toISOString() : null,
|
|
360
|
+
daysRemaining: 0
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
let daysRemaining = null;
|
|
364
|
+
if (payload.expires_at !== null) {
|
|
365
|
+
daysRemaining = Math.ceil((payload.expires_at - now) / 86400);
|
|
366
|
+
if (daysRemaining < 0) daysRemaining = 0;
|
|
367
|
+
}
|
|
368
|
+
this.storage.save({
|
|
369
|
+
...cached,
|
|
370
|
+
lastVerifiedAt: now
|
|
371
|
+
});
|
|
372
|
+
const status = {
|
|
373
|
+
valid: true,
|
|
374
|
+
tier: payload.tier,
|
|
375
|
+
features: payload.features,
|
|
376
|
+
expiresAt: payload.expires_at ? new Date(payload.expires_at * 1e3).toISOString() : null,
|
|
377
|
+
daysRemaining,
|
|
378
|
+
isOffline: true,
|
|
379
|
+
isTrial: payload.tier === "trial",
|
|
380
|
+
inGracePeriod
|
|
381
|
+
};
|
|
382
|
+
this.notifyStatusChange(status);
|
|
383
|
+
this.checkExpiryReminders(daysRemaining);
|
|
384
|
+
return status;
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* 在线验证(备用)
|
|
388
|
+
*/
|
|
389
|
+
async verifyOnline(licenseKey, identity) {
|
|
390
|
+
const identityHash = hashIdentity(identity, this.config.appSecret);
|
|
391
|
+
const body = JSON.stringify({
|
|
392
|
+
license_key: licenseKey,
|
|
393
|
+
identity_hash: identityHash
|
|
394
|
+
});
|
|
395
|
+
try {
|
|
396
|
+
const data = await this.apiPost("/api/v1/sdk/verify", body);
|
|
397
|
+
if (data.offline_token) {
|
|
398
|
+
this.storage.save({
|
|
399
|
+
offlineToken: data.offline_token,
|
|
400
|
+
lastVerifiedAt: Math.floor(Date.now() / 1e3),
|
|
401
|
+
tokenVersion: 0,
|
|
402
|
+
licenseKey
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
const status = {
|
|
406
|
+
valid: data.valid,
|
|
407
|
+
tier: data.tier,
|
|
408
|
+
features: data.features,
|
|
409
|
+
expiresAt: data.expires_at,
|
|
410
|
+
daysRemaining: data.days_remaining,
|
|
411
|
+
isOffline: false,
|
|
412
|
+
isTrial: data.tier === "trial",
|
|
413
|
+
inGracePeriod: false
|
|
414
|
+
};
|
|
415
|
+
this.notifyStatusChange(status);
|
|
416
|
+
return status;
|
|
417
|
+
} catch (err) {
|
|
418
|
+
return {
|
|
419
|
+
valid: false,
|
|
420
|
+
tier: "",
|
|
421
|
+
features: [],
|
|
422
|
+
expiresAt: null,
|
|
423
|
+
daysRemaining: null,
|
|
424
|
+
isOffline: true,
|
|
425
|
+
isTrial: false,
|
|
426
|
+
inGracePeriod: false
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* 检查是否有某个功能
|
|
432
|
+
*/
|
|
433
|
+
hasFeature(featureName) {
|
|
434
|
+
const cached = this.storage.load();
|
|
435
|
+
if (!cached?.offlineToken) return false;
|
|
436
|
+
const payload = verifyOfflineToken(
|
|
437
|
+
cached.offlineToken,
|
|
438
|
+
this.config.publicKey
|
|
439
|
+
);
|
|
440
|
+
if (!payload) return false;
|
|
441
|
+
return payload.features.includes(featureName);
|
|
442
|
+
}
|
|
443
|
+
// ===== 支付 =====
|
|
444
|
+
/**
|
|
445
|
+
* 获取定价方案列表
|
|
446
|
+
*/
|
|
447
|
+
async getPricingPlans() {
|
|
448
|
+
const url = `${this.config.baseUrl}/api/v1/public/app-info?app_id=${encodeURIComponent(this.config.appId)}`;
|
|
449
|
+
const response = await fetch(url);
|
|
450
|
+
if (!response.ok) {
|
|
451
|
+
throw new ClavisError(
|
|
452
|
+
ErrorCodes.API_ERROR,
|
|
453
|
+
`\u83B7\u53D6\u5B9A\u4EF7\u65B9\u6848\u5931\u8D25: ${response.status}`
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
const json = await response.json();
|
|
457
|
+
if (!json.success || !json.data) {
|
|
458
|
+
throw new ClavisError(
|
|
459
|
+
ErrorCodes.API_ERROR,
|
|
460
|
+
json.error?.message || "\u83B7\u53D6\u5B9A\u4EF7\u65B9\u6848\u5931\u8D25"
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
return json.data.pricing || [];
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* 获取支付页面 URL
|
|
467
|
+
*/
|
|
468
|
+
getPaymentUrl(planId, identity) {
|
|
469
|
+
let url = `${this.config.baseUrl}/activate/${this.config.appId}?plan=${encodeURIComponent(planId)}`;
|
|
470
|
+
if (identity) {
|
|
471
|
+
url += `&identity=${encodeURIComponent(identity)}`;
|
|
472
|
+
}
|
|
473
|
+
return url;
|
|
474
|
+
}
|
|
475
|
+
// ===== 生命周期 =====
|
|
476
|
+
/**
|
|
477
|
+
* 解绑当前设备(需要联网)
|
|
478
|
+
*/
|
|
479
|
+
async deactivate(licenseKey, identity) {
|
|
480
|
+
const identityHash = hashIdentity(identity, this.config.appSecret);
|
|
481
|
+
const body = JSON.stringify({
|
|
482
|
+
license_key: licenseKey,
|
|
483
|
+
identity_hash: identityHash
|
|
484
|
+
});
|
|
485
|
+
await this.apiPost("/api/v1/sdk/deactivate", body);
|
|
486
|
+
this.storage.clear();
|
|
487
|
+
this.stopHeartbeat();
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* 心跳(通常自动调用)
|
|
491
|
+
*/
|
|
492
|
+
async heartbeat(licenseKey, identity) {
|
|
493
|
+
const identityHash = hashIdentity(identity, this.config.appSecret);
|
|
494
|
+
const body = JSON.stringify({
|
|
495
|
+
license_key: licenseKey,
|
|
496
|
+
identity_hash: identityHash
|
|
497
|
+
});
|
|
498
|
+
const data = await this.apiPost("/api/v1/sdk/heartbeat", body);
|
|
499
|
+
if (data.offline_token) {
|
|
500
|
+
this.storage.save({
|
|
501
|
+
offlineToken: data.offline_token,
|
|
502
|
+
lastVerifiedAt: Math.floor(Date.now() / 1e3),
|
|
503
|
+
tokenVersion: 0,
|
|
504
|
+
licenseKey
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
return {
|
|
508
|
+
serverTimestamp: data.server_timestamp,
|
|
509
|
+
status: data.status,
|
|
510
|
+
offlineToken: data.offline_token
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* 监听授权状态变化
|
|
515
|
+
* @returns 取消监听函数
|
|
516
|
+
*/
|
|
517
|
+
onStatusChange(callback) {
|
|
518
|
+
this.statusListeners.push(callback);
|
|
519
|
+
return () => {
|
|
520
|
+
this.statusListeners = this.statusListeners.filter(
|
|
521
|
+
(cb) => cb !== callback
|
|
522
|
+
);
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* 到期前提醒
|
|
527
|
+
* @param daysBeforeExpiry 到期前多少天触发
|
|
528
|
+
* @returns 取消监听函数
|
|
529
|
+
*/
|
|
530
|
+
onExpiring(daysBeforeExpiry, callback) {
|
|
531
|
+
const listener = { days: daysBeforeExpiry, callback, fired: false };
|
|
532
|
+
this.expiryListeners.push(listener);
|
|
533
|
+
return () => {
|
|
534
|
+
this.expiryListeners = this.expiryListeners.filter(
|
|
535
|
+
(l) => l !== listener
|
|
536
|
+
);
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* 销毁(清理心跳定时器和监听器)
|
|
541
|
+
*/
|
|
542
|
+
destroy() {
|
|
543
|
+
this.stopHeartbeat();
|
|
544
|
+
this.statusListeners = [];
|
|
545
|
+
this.expiryListeners = [];
|
|
546
|
+
this.lastStatus = null;
|
|
547
|
+
}
|
|
548
|
+
// ===== 内部方法 =====
|
|
549
|
+
/** 发送带 HMAC 签名的 POST 请求 */
|
|
550
|
+
async apiPost(path2, body) {
|
|
551
|
+
const headers = signRequest({
|
|
552
|
+
method: "POST",
|
|
553
|
+
path: path2,
|
|
554
|
+
body,
|
|
555
|
+
appId: this.config.appId,
|
|
556
|
+
appSecret: this.config.appSecret
|
|
557
|
+
});
|
|
558
|
+
const url = `${this.config.baseUrl}${path2}`;
|
|
559
|
+
let response;
|
|
560
|
+
try {
|
|
561
|
+
response = await fetch(url, {
|
|
562
|
+
method: "POST",
|
|
563
|
+
headers: {
|
|
564
|
+
...headers,
|
|
565
|
+
"Content-Type": "application/json"
|
|
566
|
+
},
|
|
567
|
+
body
|
|
568
|
+
});
|
|
569
|
+
} catch (err) {
|
|
570
|
+
throw new ClavisError(
|
|
571
|
+
ErrorCodes.NETWORK_ERROR,
|
|
572
|
+
`\u7F51\u7EDC\u8BF7\u6C42\u5931\u8D25: ${err instanceof Error ? err.message : String(err)}`
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
const json = await response.json();
|
|
576
|
+
if (!response.ok || !json.success) {
|
|
577
|
+
const code = json.error?.code || ErrorCodes.API_ERROR;
|
|
578
|
+
const message = json.error?.message || `API \u8BF7\u6C42\u5931\u8D25: ${response.status}`;
|
|
579
|
+
throw new ClavisError(code, message);
|
|
580
|
+
}
|
|
581
|
+
return json.data;
|
|
582
|
+
}
|
|
583
|
+
/** 启动自动心跳 */
|
|
584
|
+
startHeartbeat(licenseKey, identity) {
|
|
585
|
+
this.stopHeartbeat();
|
|
586
|
+
this.heartbeatTimer = setInterval(async () => {
|
|
587
|
+
try {
|
|
588
|
+
await this.heartbeat(licenseKey, identity);
|
|
589
|
+
} catch {
|
|
590
|
+
}
|
|
591
|
+
}, this.config.heartbeatInterval * 1e3);
|
|
592
|
+
if (this.heartbeatTimer && typeof this.heartbeatTimer === "object" && "unref" in this.heartbeatTimer) {
|
|
593
|
+
this.heartbeatTimer.unref();
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
/** 停止心跳 */
|
|
597
|
+
stopHeartbeat() {
|
|
598
|
+
if (this.heartbeatTimer) {
|
|
599
|
+
clearInterval(this.heartbeatTimer);
|
|
600
|
+
this.heartbeatTimer = void 0;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
/** 通知状态变化 */
|
|
604
|
+
notifyStatusChange(status) {
|
|
605
|
+
if (this.lastStatus === null || this.lastStatus.valid !== status.valid || this.lastStatus.tier !== status.tier) {
|
|
606
|
+
this.lastStatus = status;
|
|
607
|
+
for (const cb of this.statusListeners) {
|
|
608
|
+
try {
|
|
609
|
+
cb(status);
|
|
610
|
+
} catch {
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
/** 检查到期提醒 */
|
|
616
|
+
checkExpiryReminders(daysRemaining) {
|
|
617
|
+
if (daysRemaining === null) return;
|
|
618
|
+
for (const listener of this.expiryListeners) {
|
|
619
|
+
if (!listener.fired && daysRemaining <= listener.days) {
|
|
620
|
+
listener.fired = true;
|
|
621
|
+
try {
|
|
622
|
+
listener.callback();
|
|
623
|
+
} catch {
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
/** 从激活结果构造 LicenseStatus */
|
|
629
|
+
buildStatusFromActivate(result, isOffline) {
|
|
630
|
+
let daysRemaining = null;
|
|
631
|
+
if (result.expiresAt) {
|
|
632
|
+
const expiresMs = new Date(result.expiresAt).getTime();
|
|
633
|
+
daysRemaining = Math.ceil((expiresMs - Date.now()) / (86400 * 1e3));
|
|
634
|
+
if (daysRemaining < 0) daysRemaining = 0;
|
|
635
|
+
}
|
|
636
|
+
return {
|
|
637
|
+
valid: result.success,
|
|
638
|
+
tier: result.tier,
|
|
639
|
+
features: result.features,
|
|
640
|
+
expiresAt: result.expiresAt,
|
|
641
|
+
daysRemaining,
|
|
642
|
+
isOffline,
|
|
643
|
+
isTrial: result.tier === "trial",
|
|
644
|
+
inGracePeriod: false
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
// src/index.ts
|
|
650
|
+
var index_default = Clavis;
|
|
651
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
652
|
+
0 && (module.exports = {
|
|
653
|
+
Clavis,
|
|
654
|
+
ClavisError,
|
|
655
|
+
ErrorCodes,
|
|
656
|
+
FileStorage,
|
|
657
|
+
getDeviceInfo,
|
|
658
|
+
getMachineCode,
|
|
659
|
+
hashIdentity,
|
|
660
|
+
signRequest,
|
|
661
|
+
verifyOfflineToken
|
|
662
|
+
});
|