@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/README.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# @clavis/sdk
|
|
2
|
+
|
|
3
|
+
Clavis 软件授权 SDK — 离线验证 + HMAC 签名 + 设备指纹
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @clavis/sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## 快速开始
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { Clavis } from "@clavis/sdk";
|
|
15
|
+
|
|
16
|
+
// 初始化
|
|
17
|
+
const clavis = new Clavis({
|
|
18
|
+
appId: "nc1a2b3c4d5e6f7g8h", // 从 Clavis 管理后台获取
|
|
19
|
+
appSecret: "your-app-secret", // 从 Clavis 管理后台获取
|
|
20
|
+
publicKey: "your-ed25519-public-key", // 从 Clavis 管理后台获取
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// 获取机器码
|
|
24
|
+
const machineCode = Clavis.getMachineCode();
|
|
25
|
+
|
|
26
|
+
// 激活
|
|
27
|
+
const result = await clavis.activate("XXXXX-XXXXX-XXXXX-XXXXX-XXXXX", machineCode);
|
|
28
|
+
|
|
29
|
+
// 离线验证(零网络请求)
|
|
30
|
+
const status = clavis.verifyOffline(machineCode);
|
|
31
|
+
console.log(status.valid, status.tier, status.features);
|
|
32
|
+
|
|
33
|
+
// 销毁
|
|
34
|
+
clavis.destroy();
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## API
|
|
38
|
+
|
|
39
|
+
### `new Clavis(config)`
|
|
40
|
+
|
|
41
|
+
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|
|
42
|
+
|------|------|------|--------|------|
|
|
43
|
+
| appId | string | 是 | - | 应用 ID |
|
|
44
|
+
| appSecret | string | 是 | - | 应用密钥 |
|
|
45
|
+
| publicKey | string | 是 | - | Ed25519 公钥 (base64) |
|
|
46
|
+
| baseUrl | string | 否 | `https://c.nietzsci.com` | API 地址 |
|
|
47
|
+
| storagePath | string | 否 | `process.cwd()` | 缓存路径 |
|
|
48
|
+
| offlineGraceDays | number | 否 | 7 | 离线宽限天数 |
|
|
49
|
+
| heartbeatInterval | number | 否 | 3600 | 心跳间隔(秒) |
|
|
50
|
+
| autoHeartbeat | boolean | 否 | true | 自动心跳 |
|
|
51
|
+
|
|
52
|
+
### 激活
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
const result = await clavis.activate(licenseKey, identity);
|
|
56
|
+
// result: { success, tier, features, expiresAt, offlineToken }
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 离线验证
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
const status = clavis.verifyOffline(identity);
|
|
63
|
+
// status: { valid, tier, features, expiresAt, daysRemaining, isOffline, isTrial, inGracePeriod }
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### 混合验证(优先离线)
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
const status = await clavis.checkLicense(identity);
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 功能检查
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
if (clavis.hasFeature("dark_mode")) {
|
|
76
|
+
// 启用深色模式
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### 事件监听
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
// 状态变化
|
|
84
|
+
const unsub = clavis.onStatusChange((status) => {
|
|
85
|
+
console.log("授权状态:", status);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// 到期提醒
|
|
89
|
+
clavis.onExpiring(7, () => {
|
|
90
|
+
console.log("即将到期!");
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### 解绑
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
await clavis.deactivate(licenseKey, identity);
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### 支付
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
const plans = await clavis.getPricingPlans();
|
|
104
|
+
const url = clavis.getPaymentUrl(planId, identity);
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## 工具函数
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
import { getMachineCode, getDeviceInfo, verifyOfflineToken, hashIdentity } from "@clavis/sdk";
|
|
111
|
+
|
|
112
|
+
// 设备指纹
|
|
113
|
+
const code = getMachineCode(); // MC-XXXX-XXXX-XXXX-XXXX
|
|
114
|
+
const info = getDeviceInfo(); // { os, osVersion, hostname, ... }
|
|
115
|
+
|
|
116
|
+
// 离线验签
|
|
117
|
+
const payload = verifyOfflineToken(token, publicKey);
|
|
118
|
+
|
|
119
|
+
// identity 哈希
|
|
120
|
+
const hash = hashIdentity(identity, appSecret);
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## 安全说明
|
|
124
|
+
|
|
125
|
+
- `appSecret` 不要暴露在前端代码中,仅在 Node.js 服务端使用
|
|
126
|
+
- 浏览器端只能做离线验证(公钥验签),激活/心跳/解绑需通过后端代理
|
|
127
|
+
- 本地缓存使用 AES-256-GCM 加密,防止直接读取 token
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/** SDK 初始化配置 */
|
|
2
|
+
interface ClavisConfig {
|
|
3
|
+
/** 应用 ID,格式 nc_xxx */
|
|
4
|
+
appId: string;
|
|
5
|
+
/** 应用密钥,用于 HMAC 签名(服务端 SDK 使用,纯客户端不传) */
|
|
6
|
+
appSecret: string;
|
|
7
|
+
/** Ed25519 公钥(标准 base64),用于离线验证 */
|
|
8
|
+
publicKey: string;
|
|
9
|
+
/** API 地址,默认 https://c.nietzsci.com */
|
|
10
|
+
baseUrl?: string;
|
|
11
|
+
/** 本地缓存路径(Node.js 环境),默认 process.cwd() */
|
|
12
|
+
storagePath?: string;
|
|
13
|
+
/** 离线宽限天数,默认 7 */
|
|
14
|
+
offlineGraceDays?: number;
|
|
15
|
+
/** 心跳间隔(秒),默认 3600 */
|
|
16
|
+
heartbeatInterval?: number;
|
|
17
|
+
/** 是否自动心跳,默认 true */
|
|
18
|
+
autoHeartbeat?: boolean;
|
|
19
|
+
}
|
|
20
|
+
/** 激活结果 */
|
|
21
|
+
interface ActivateResult {
|
|
22
|
+
success: boolean;
|
|
23
|
+
tier: string;
|
|
24
|
+
features: string[];
|
|
25
|
+
expiresAt: string | null;
|
|
26
|
+
offlineToken: string;
|
|
27
|
+
}
|
|
28
|
+
/** 授权状态 */
|
|
29
|
+
interface LicenseStatus {
|
|
30
|
+
valid: boolean;
|
|
31
|
+
tier: string;
|
|
32
|
+
features: string[];
|
|
33
|
+
expiresAt: string | null;
|
|
34
|
+
daysRemaining: number | null;
|
|
35
|
+
isOffline: boolean;
|
|
36
|
+
isTrial: boolean;
|
|
37
|
+
inGracePeriod: boolean;
|
|
38
|
+
}
|
|
39
|
+
/** 离线 Token Payload(与服务端 signPayload 的 payload 完全一致) */
|
|
40
|
+
interface TokenPayload {
|
|
41
|
+
app_id: string;
|
|
42
|
+
license_id: string;
|
|
43
|
+
identity_hash: string;
|
|
44
|
+
tier: string;
|
|
45
|
+
features: string[];
|
|
46
|
+
issued_at: number;
|
|
47
|
+
expires_at: number | null;
|
|
48
|
+
grace_until: number;
|
|
49
|
+
token_version: number;
|
|
50
|
+
}
|
|
51
|
+
/** 心跳结果 */
|
|
52
|
+
interface HeartbeatResult {
|
|
53
|
+
serverTimestamp: number;
|
|
54
|
+
status: string;
|
|
55
|
+
offlineToken?: string;
|
|
56
|
+
}
|
|
57
|
+
/** 定价方案 */
|
|
58
|
+
interface PricingPlan {
|
|
59
|
+
id: string;
|
|
60
|
+
tier: string;
|
|
61
|
+
name: string;
|
|
62
|
+
duration: string;
|
|
63
|
+
durationDays: number;
|
|
64
|
+
price: number;
|
|
65
|
+
}
|
|
66
|
+
/** 设备信息 */
|
|
67
|
+
interface DeviceInfo {
|
|
68
|
+
os: string;
|
|
69
|
+
osVersion: string;
|
|
70
|
+
hostname: string;
|
|
71
|
+
arch: string;
|
|
72
|
+
cpuModel: string;
|
|
73
|
+
totalMemory: number;
|
|
74
|
+
}
|
|
75
|
+
/** 本地缓存数据结构 */
|
|
76
|
+
interface CacheData {
|
|
77
|
+
offlineToken: string;
|
|
78
|
+
lastVerifiedAt: number;
|
|
79
|
+
tokenVersion: number;
|
|
80
|
+
licenseKey: string;
|
|
81
|
+
}
|
|
82
|
+
/** API 响应通用结构 */
|
|
83
|
+
interface ApiResponse<T = unknown> {
|
|
84
|
+
success: boolean;
|
|
85
|
+
data?: T;
|
|
86
|
+
error?: {
|
|
87
|
+
code: string;
|
|
88
|
+
message: string;
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
type StatusChangeCallback = (status: LicenseStatus) => void;
|
|
93
|
+
declare class Clavis {
|
|
94
|
+
private config;
|
|
95
|
+
private storage;
|
|
96
|
+
private heartbeatTimer?;
|
|
97
|
+
private statusListeners;
|
|
98
|
+
private expiryListeners;
|
|
99
|
+
private lastStatus;
|
|
100
|
+
private currentLicenseKey;
|
|
101
|
+
private currentIdentityHash;
|
|
102
|
+
constructor(config: ClavisConfig);
|
|
103
|
+
/** 生成设备机器码 */
|
|
104
|
+
static getMachineCode(appSecret?: string): string;
|
|
105
|
+
/** 获取设备信息 */
|
|
106
|
+
static getDeviceInfo(): DeviceInfo;
|
|
107
|
+
/**
|
|
108
|
+
* 激活授权码(需要联网)
|
|
109
|
+
*
|
|
110
|
+
* @param licenseKey 授权码,格式 XXXXX-XXXXX-XXXXX-XXXXX-XXXXX
|
|
111
|
+
* @param identity 设备标识(机器码或自定义字符串),SDK 会自动 HMAC 哈希
|
|
112
|
+
*/
|
|
113
|
+
activate(licenseKey: string, identity: string): Promise<ActivateResult>;
|
|
114
|
+
/**
|
|
115
|
+
* 检查授权状态(优先离线,失败则在线)
|
|
116
|
+
*/
|
|
117
|
+
checkLicense(identity: string): Promise<LicenseStatus>;
|
|
118
|
+
/**
|
|
119
|
+
* 纯离线验证(零网络请求)
|
|
120
|
+
*
|
|
121
|
+
* @param identity 可选的设备标识,用于 identity_hash 匹配校验
|
|
122
|
+
*/
|
|
123
|
+
verifyOffline(identity?: string): LicenseStatus;
|
|
124
|
+
/**
|
|
125
|
+
* 在线验证(备用)
|
|
126
|
+
*/
|
|
127
|
+
verifyOnline(licenseKey: string, identity: string): Promise<LicenseStatus>;
|
|
128
|
+
/**
|
|
129
|
+
* 检查是否有某个功能
|
|
130
|
+
*/
|
|
131
|
+
hasFeature(featureName: string): boolean;
|
|
132
|
+
/**
|
|
133
|
+
* 获取定价方案列表
|
|
134
|
+
*/
|
|
135
|
+
getPricingPlans(): Promise<PricingPlan[]>;
|
|
136
|
+
/**
|
|
137
|
+
* 获取支付页面 URL
|
|
138
|
+
*/
|
|
139
|
+
getPaymentUrl(planId: string, identity?: string): string;
|
|
140
|
+
/**
|
|
141
|
+
* 解绑当前设备(需要联网)
|
|
142
|
+
*/
|
|
143
|
+
deactivate(licenseKey: string, identity: string): Promise<void>;
|
|
144
|
+
/**
|
|
145
|
+
* 心跳(通常自动调用)
|
|
146
|
+
*/
|
|
147
|
+
heartbeat(licenseKey: string, identity: string): Promise<HeartbeatResult>;
|
|
148
|
+
/**
|
|
149
|
+
* 监听授权状态变化
|
|
150
|
+
* @returns 取消监听函数
|
|
151
|
+
*/
|
|
152
|
+
onStatusChange(callback: StatusChangeCallback): () => void;
|
|
153
|
+
/**
|
|
154
|
+
* 到期前提醒
|
|
155
|
+
* @param daysBeforeExpiry 到期前多少天触发
|
|
156
|
+
* @returns 取消监听函数
|
|
157
|
+
*/
|
|
158
|
+
onExpiring(daysBeforeExpiry: number, callback: () => void): () => void;
|
|
159
|
+
/**
|
|
160
|
+
* 销毁(清理心跳定时器和监听器)
|
|
161
|
+
*/
|
|
162
|
+
destroy(): void;
|
|
163
|
+
/** 发送带 HMAC 签名的 POST 请求 */
|
|
164
|
+
private apiPost;
|
|
165
|
+
/** 启动自动心跳 */
|
|
166
|
+
private startHeartbeat;
|
|
167
|
+
/** 停止心跳 */
|
|
168
|
+
private stopHeartbeat;
|
|
169
|
+
/** 通知状态变化 */
|
|
170
|
+
private notifyStatusChange;
|
|
171
|
+
/** 检查到期提醒 */
|
|
172
|
+
private checkExpiryReminders;
|
|
173
|
+
/** 从激活结果构造 LicenseStatus */
|
|
174
|
+
private buildStatusFromActivate;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
declare class ClavisError extends Error {
|
|
178
|
+
code: string;
|
|
179
|
+
constructor(code: string, message: string);
|
|
180
|
+
}
|
|
181
|
+
/** 错误码枚举 */
|
|
182
|
+
declare const ErrorCodes: {
|
|
183
|
+
readonly NETWORK_ERROR: "NETWORK_ERROR";
|
|
184
|
+
readonly API_ERROR: "API_ERROR";
|
|
185
|
+
readonly INVALID_SIGNATURE: "INVALID_SIGNATURE";
|
|
186
|
+
readonly MISSING_AUTH_HEADERS: "MISSING_AUTH_HEADERS";
|
|
187
|
+
readonly TIMESTAMP_EXPIRED: "TIMESTAMP_EXPIRED";
|
|
188
|
+
readonly LICENSE_NOT_FOUND: "LICENSE_NOT_FOUND";
|
|
189
|
+
readonly LICENSE_EXPIRED: "LICENSE_EXPIRED";
|
|
190
|
+
readonly LICENSE_REVOKED: "LICENSE_REVOKED";
|
|
191
|
+
readonly ACTIVATION_LIMIT: "ACTIVATION_LIMIT";
|
|
192
|
+
readonly ACTIVATION_NOT_FOUND: "ACTIVATION_NOT_FOUND";
|
|
193
|
+
readonly NO_CACHED_TOKEN: "NO_CACHED_TOKEN";
|
|
194
|
+
readonly TOKEN_SIGNATURE_INVALID: "TOKEN_SIGNATURE_INVALID";
|
|
195
|
+
readonly TOKEN_EXPIRED: "TOKEN_EXPIRED";
|
|
196
|
+
readonly TOKEN_GRACE_EXPIRED: "TOKEN_GRACE_EXPIRED";
|
|
197
|
+
readonly IDENTITY_MISMATCH: "IDENTITY_MISMATCH";
|
|
198
|
+
readonly CLOCK_DRIFT_DETECTED: "CLOCK_DRIFT_DETECTED";
|
|
199
|
+
readonly MISSING_CONFIG: "MISSING_CONFIG";
|
|
200
|
+
readonly INVALID_CONFIG: "INVALID_CONFIG";
|
|
201
|
+
};
|
|
202
|
+
type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes];
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* 验证 offline token 的 Ed25519 签名
|
|
206
|
+
*
|
|
207
|
+
* token 格式: base64url(payload_json).base64url(signature_64bytes)
|
|
208
|
+
*
|
|
209
|
+
* 签名流程(服务端 signPayload):
|
|
210
|
+
* 1. payloadJson = JSON.stringify(payload)
|
|
211
|
+
* 2. messageBytes = TextEncoder.encode(payloadJson)
|
|
212
|
+
* 3. signedMessage = nacl.sign(messageBytes, privateKey) → 64字节签名 + 消息
|
|
213
|
+
* 4. signature = signedMessage.slice(0, 64)
|
|
214
|
+
* 5. token = base64url(payloadJson) + "." + base64url(signature)
|
|
215
|
+
*
|
|
216
|
+
* 验签流程(本函数):
|
|
217
|
+
* 1. 从 token 拆出 payloadBase64Url 和 signatureBase64Url
|
|
218
|
+
* 2. payloadJson = base64url_decode(payloadBase64Url) → UTF-8 字符串
|
|
219
|
+
* 3. messageBytes = TextEncoder.encode(payloadJson)
|
|
220
|
+
* 4. signatureBytes = base64url_decode(signatureBase64Url) → 64 字节
|
|
221
|
+
* 5. 构造 signedMessage = signature + message
|
|
222
|
+
* 6. nacl.sign.open(signedMessage, publicKey) → 验证成功则返回 message
|
|
223
|
+
* 7. 解析 payloadJson 为 TokenPayload
|
|
224
|
+
*/
|
|
225
|
+
declare function verifyOfflineToken(token: string, publicKeyBase64: string): TokenPayload | null;
|
|
226
|
+
|
|
227
|
+
interface SignedHeaders {
|
|
228
|
+
"x-app-id": string;
|
|
229
|
+
"x-timestamp": string;
|
|
230
|
+
"x-nonce": string;
|
|
231
|
+
"x-signature": string;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* 生成 HMAC 签名请求头
|
|
235
|
+
*
|
|
236
|
+
* 签名算法:
|
|
237
|
+
* string_to_sign = "${METHOD}\n${path}\n${timestamp}\n${nonce}\n${SHA256(body)}"
|
|
238
|
+
* signature = HMAC_SHA256(appSecret, string_to_sign).hex()
|
|
239
|
+
*
|
|
240
|
+
* 服务端验证时:
|
|
241
|
+
* - 时间窗口 ±300 秒
|
|
242
|
+
* - Nonce 去重(Redis SET NX,TTL 600 秒)
|
|
243
|
+
* - timingSafeEqual 比较签名
|
|
244
|
+
*/
|
|
245
|
+
declare function signRequest(params: {
|
|
246
|
+
method: string;
|
|
247
|
+
path: string;
|
|
248
|
+
body: string;
|
|
249
|
+
appId: string;
|
|
250
|
+
appSecret: string;
|
|
251
|
+
}): SignedHeaders;
|
|
252
|
+
/**
|
|
253
|
+
* 生成 identity hash
|
|
254
|
+
*
|
|
255
|
+
* 服务端期望的 identity_hash = HMAC-SHA256(appSecret, identity).hex()
|
|
256
|
+
*/
|
|
257
|
+
declare function hashIdentity(identity: string, appSecret: string): string;
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* 生成设备机器码
|
|
261
|
+
*
|
|
262
|
+
* 取硬件特征的 HMAC-SHA256 哈希,格式化为 MC-XXXX-XXXX-XXXX-XXXX
|
|
263
|
+
* 同一台机器多次调用结果一致(确定性)
|
|
264
|
+
*/
|
|
265
|
+
declare function getMachineCode(appSecret?: string): string;
|
|
266
|
+
/**
|
|
267
|
+
* 获取设备信息
|
|
268
|
+
*/
|
|
269
|
+
declare function getDeviceInfo(): DeviceInfo;
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* 文件存储
|
|
273
|
+
*
|
|
274
|
+
* 缓存内容用 AES-256-GCM 加密,密钥由 identity 派生。
|
|
275
|
+
* 换设备(identity 不同)后旧缓存自动失效。
|
|
276
|
+
*
|
|
277
|
+
* 文件格式:iv(12) + tag(16) + ciphertext
|
|
278
|
+
*/
|
|
279
|
+
declare class FileStorage {
|
|
280
|
+
private filePath;
|
|
281
|
+
private encryptionKey;
|
|
282
|
+
constructor(storagePath: string, appId: string);
|
|
283
|
+
/** 保存缓存数据(AES-256-GCM 加密) */
|
|
284
|
+
save(data: CacheData): void;
|
|
285
|
+
/** 读取缓存数据(解密) */
|
|
286
|
+
load(): CacheData | null;
|
|
287
|
+
/** 清除缓存文件 */
|
|
288
|
+
clear(): void;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export { type ActivateResult, type ApiResponse, type CacheData, Clavis, type ClavisConfig, ClavisError, type DeviceInfo, type ErrorCode, ErrorCodes, FileStorage, type HeartbeatResult, type LicenseStatus, type PricingPlan, type TokenPayload, Clavis as default, getDeviceInfo, getMachineCode, hashIdentity, signRequest, verifyOfflineToken };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/** SDK 初始化配置 */
|
|
2
|
+
interface ClavisConfig {
|
|
3
|
+
/** 应用 ID,格式 nc_xxx */
|
|
4
|
+
appId: string;
|
|
5
|
+
/** 应用密钥,用于 HMAC 签名(服务端 SDK 使用,纯客户端不传) */
|
|
6
|
+
appSecret: string;
|
|
7
|
+
/** Ed25519 公钥(标准 base64),用于离线验证 */
|
|
8
|
+
publicKey: string;
|
|
9
|
+
/** API 地址,默认 https://c.nietzsci.com */
|
|
10
|
+
baseUrl?: string;
|
|
11
|
+
/** 本地缓存路径(Node.js 环境),默认 process.cwd() */
|
|
12
|
+
storagePath?: string;
|
|
13
|
+
/** 离线宽限天数,默认 7 */
|
|
14
|
+
offlineGraceDays?: number;
|
|
15
|
+
/** 心跳间隔(秒),默认 3600 */
|
|
16
|
+
heartbeatInterval?: number;
|
|
17
|
+
/** 是否自动心跳,默认 true */
|
|
18
|
+
autoHeartbeat?: boolean;
|
|
19
|
+
}
|
|
20
|
+
/** 激活结果 */
|
|
21
|
+
interface ActivateResult {
|
|
22
|
+
success: boolean;
|
|
23
|
+
tier: string;
|
|
24
|
+
features: string[];
|
|
25
|
+
expiresAt: string | null;
|
|
26
|
+
offlineToken: string;
|
|
27
|
+
}
|
|
28
|
+
/** 授权状态 */
|
|
29
|
+
interface LicenseStatus {
|
|
30
|
+
valid: boolean;
|
|
31
|
+
tier: string;
|
|
32
|
+
features: string[];
|
|
33
|
+
expiresAt: string | null;
|
|
34
|
+
daysRemaining: number | null;
|
|
35
|
+
isOffline: boolean;
|
|
36
|
+
isTrial: boolean;
|
|
37
|
+
inGracePeriod: boolean;
|
|
38
|
+
}
|
|
39
|
+
/** 离线 Token Payload(与服务端 signPayload 的 payload 完全一致) */
|
|
40
|
+
interface TokenPayload {
|
|
41
|
+
app_id: string;
|
|
42
|
+
license_id: string;
|
|
43
|
+
identity_hash: string;
|
|
44
|
+
tier: string;
|
|
45
|
+
features: string[];
|
|
46
|
+
issued_at: number;
|
|
47
|
+
expires_at: number | null;
|
|
48
|
+
grace_until: number;
|
|
49
|
+
token_version: number;
|
|
50
|
+
}
|
|
51
|
+
/** 心跳结果 */
|
|
52
|
+
interface HeartbeatResult {
|
|
53
|
+
serverTimestamp: number;
|
|
54
|
+
status: string;
|
|
55
|
+
offlineToken?: string;
|
|
56
|
+
}
|
|
57
|
+
/** 定价方案 */
|
|
58
|
+
interface PricingPlan {
|
|
59
|
+
id: string;
|
|
60
|
+
tier: string;
|
|
61
|
+
name: string;
|
|
62
|
+
duration: string;
|
|
63
|
+
durationDays: number;
|
|
64
|
+
price: number;
|
|
65
|
+
}
|
|
66
|
+
/** 设备信息 */
|
|
67
|
+
interface DeviceInfo {
|
|
68
|
+
os: string;
|
|
69
|
+
osVersion: string;
|
|
70
|
+
hostname: string;
|
|
71
|
+
arch: string;
|
|
72
|
+
cpuModel: string;
|
|
73
|
+
totalMemory: number;
|
|
74
|
+
}
|
|
75
|
+
/** 本地缓存数据结构 */
|
|
76
|
+
interface CacheData {
|
|
77
|
+
offlineToken: string;
|
|
78
|
+
lastVerifiedAt: number;
|
|
79
|
+
tokenVersion: number;
|
|
80
|
+
licenseKey: string;
|
|
81
|
+
}
|
|
82
|
+
/** API 响应通用结构 */
|
|
83
|
+
interface ApiResponse<T = unknown> {
|
|
84
|
+
success: boolean;
|
|
85
|
+
data?: T;
|
|
86
|
+
error?: {
|
|
87
|
+
code: string;
|
|
88
|
+
message: string;
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
type StatusChangeCallback = (status: LicenseStatus) => void;
|
|
93
|
+
declare class Clavis {
|
|
94
|
+
private config;
|
|
95
|
+
private storage;
|
|
96
|
+
private heartbeatTimer?;
|
|
97
|
+
private statusListeners;
|
|
98
|
+
private expiryListeners;
|
|
99
|
+
private lastStatus;
|
|
100
|
+
private currentLicenseKey;
|
|
101
|
+
private currentIdentityHash;
|
|
102
|
+
constructor(config: ClavisConfig);
|
|
103
|
+
/** 生成设备机器码 */
|
|
104
|
+
static getMachineCode(appSecret?: string): string;
|
|
105
|
+
/** 获取设备信息 */
|
|
106
|
+
static getDeviceInfo(): DeviceInfo;
|
|
107
|
+
/**
|
|
108
|
+
* 激活授权码(需要联网)
|
|
109
|
+
*
|
|
110
|
+
* @param licenseKey 授权码,格式 XXXXX-XXXXX-XXXXX-XXXXX-XXXXX
|
|
111
|
+
* @param identity 设备标识(机器码或自定义字符串),SDK 会自动 HMAC 哈希
|
|
112
|
+
*/
|
|
113
|
+
activate(licenseKey: string, identity: string): Promise<ActivateResult>;
|
|
114
|
+
/**
|
|
115
|
+
* 检查授权状态(优先离线,失败则在线)
|
|
116
|
+
*/
|
|
117
|
+
checkLicense(identity: string): Promise<LicenseStatus>;
|
|
118
|
+
/**
|
|
119
|
+
* 纯离线验证(零网络请求)
|
|
120
|
+
*
|
|
121
|
+
* @param identity 可选的设备标识,用于 identity_hash 匹配校验
|
|
122
|
+
*/
|
|
123
|
+
verifyOffline(identity?: string): LicenseStatus;
|
|
124
|
+
/**
|
|
125
|
+
* 在线验证(备用)
|
|
126
|
+
*/
|
|
127
|
+
verifyOnline(licenseKey: string, identity: string): Promise<LicenseStatus>;
|
|
128
|
+
/**
|
|
129
|
+
* 检查是否有某个功能
|
|
130
|
+
*/
|
|
131
|
+
hasFeature(featureName: string): boolean;
|
|
132
|
+
/**
|
|
133
|
+
* 获取定价方案列表
|
|
134
|
+
*/
|
|
135
|
+
getPricingPlans(): Promise<PricingPlan[]>;
|
|
136
|
+
/**
|
|
137
|
+
* 获取支付页面 URL
|
|
138
|
+
*/
|
|
139
|
+
getPaymentUrl(planId: string, identity?: string): string;
|
|
140
|
+
/**
|
|
141
|
+
* 解绑当前设备(需要联网)
|
|
142
|
+
*/
|
|
143
|
+
deactivate(licenseKey: string, identity: string): Promise<void>;
|
|
144
|
+
/**
|
|
145
|
+
* 心跳(通常自动调用)
|
|
146
|
+
*/
|
|
147
|
+
heartbeat(licenseKey: string, identity: string): Promise<HeartbeatResult>;
|
|
148
|
+
/**
|
|
149
|
+
* 监听授权状态变化
|
|
150
|
+
* @returns 取消监听函数
|
|
151
|
+
*/
|
|
152
|
+
onStatusChange(callback: StatusChangeCallback): () => void;
|
|
153
|
+
/**
|
|
154
|
+
* 到期前提醒
|
|
155
|
+
* @param daysBeforeExpiry 到期前多少天触发
|
|
156
|
+
* @returns 取消监听函数
|
|
157
|
+
*/
|
|
158
|
+
onExpiring(daysBeforeExpiry: number, callback: () => void): () => void;
|
|
159
|
+
/**
|
|
160
|
+
* 销毁(清理心跳定时器和监听器)
|
|
161
|
+
*/
|
|
162
|
+
destroy(): void;
|
|
163
|
+
/** 发送带 HMAC 签名的 POST 请求 */
|
|
164
|
+
private apiPost;
|
|
165
|
+
/** 启动自动心跳 */
|
|
166
|
+
private startHeartbeat;
|
|
167
|
+
/** 停止心跳 */
|
|
168
|
+
private stopHeartbeat;
|
|
169
|
+
/** 通知状态变化 */
|
|
170
|
+
private notifyStatusChange;
|
|
171
|
+
/** 检查到期提醒 */
|
|
172
|
+
private checkExpiryReminders;
|
|
173
|
+
/** 从激活结果构造 LicenseStatus */
|
|
174
|
+
private buildStatusFromActivate;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
declare class ClavisError extends Error {
|
|
178
|
+
code: string;
|
|
179
|
+
constructor(code: string, message: string);
|
|
180
|
+
}
|
|
181
|
+
/** 错误码枚举 */
|
|
182
|
+
declare const ErrorCodes: {
|
|
183
|
+
readonly NETWORK_ERROR: "NETWORK_ERROR";
|
|
184
|
+
readonly API_ERROR: "API_ERROR";
|
|
185
|
+
readonly INVALID_SIGNATURE: "INVALID_SIGNATURE";
|
|
186
|
+
readonly MISSING_AUTH_HEADERS: "MISSING_AUTH_HEADERS";
|
|
187
|
+
readonly TIMESTAMP_EXPIRED: "TIMESTAMP_EXPIRED";
|
|
188
|
+
readonly LICENSE_NOT_FOUND: "LICENSE_NOT_FOUND";
|
|
189
|
+
readonly LICENSE_EXPIRED: "LICENSE_EXPIRED";
|
|
190
|
+
readonly LICENSE_REVOKED: "LICENSE_REVOKED";
|
|
191
|
+
readonly ACTIVATION_LIMIT: "ACTIVATION_LIMIT";
|
|
192
|
+
readonly ACTIVATION_NOT_FOUND: "ACTIVATION_NOT_FOUND";
|
|
193
|
+
readonly NO_CACHED_TOKEN: "NO_CACHED_TOKEN";
|
|
194
|
+
readonly TOKEN_SIGNATURE_INVALID: "TOKEN_SIGNATURE_INVALID";
|
|
195
|
+
readonly TOKEN_EXPIRED: "TOKEN_EXPIRED";
|
|
196
|
+
readonly TOKEN_GRACE_EXPIRED: "TOKEN_GRACE_EXPIRED";
|
|
197
|
+
readonly IDENTITY_MISMATCH: "IDENTITY_MISMATCH";
|
|
198
|
+
readonly CLOCK_DRIFT_DETECTED: "CLOCK_DRIFT_DETECTED";
|
|
199
|
+
readonly MISSING_CONFIG: "MISSING_CONFIG";
|
|
200
|
+
readonly INVALID_CONFIG: "INVALID_CONFIG";
|
|
201
|
+
};
|
|
202
|
+
type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes];
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* 验证 offline token 的 Ed25519 签名
|
|
206
|
+
*
|
|
207
|
+
* token 格式: base64url(payload_json).base64url(signature_64bytes)
|
|
208
|
+
*
|
|
209
|
+
* 签名流程(服务端 signPayload):
|
|
210
|
+
* 1. payloadJson = JSON.stringify(payload)
|
|
211
|
+
* 2. messageBytes = TextEncoder.encode(payloadJson)
|
|
212
|
+
* 3. signedMessage = nacl.sign(messageBytes, privateKey) → 64字节签名 + 消息
|
|
213
|
+
* 4. signature = signedMessage.slice(0, 64)
|
|
214
|
+
* 5. token = base64url(payloadJson) + "." + base64url(signature)
|
|
215
|
+
*
|
|
216
|
+
* 验签流程(本函数):
|
|
217
|
+
* 1. 从 token 拆出 payloadBase64Url 和 signatureBase64Url
|
|
218
|
+
* 2. payloadJson = base64url_decode(payloadBase64Url) → UTF-8 字符串
|
|
219
|
+
* 3. messageBytes = TextEncoder.encode(payloadJson)
|
|
220
|
+
* 4. signatureBytes = base64url_decode(signatureBase64Url) → 64 字节
|
|
221
|
+
* 5. 构造 signedMessage = signature + message
|
|
222
|
+
* 6. nacl.sign.open(signedMessage, publicKey) → 验证成功则返回 message
|
|
223
|
+
* 7. 解析 payloadJson 为 TokenPayload
|
|
224
|
+
*/
|
|
225
|
+
declare function verifyOfflineToken(token: string, publicKeyBase64: string): TokenPayload | null;
|
|
226
|
+
|
|
227
|
+
interface SignedHeaders {
|
|
228
|
+
"x-app-id": string;
|
|
229
|
+
"x-timestamp": string;
|
|
230
|
+
"x-nonce": string;
|
|
231
|
+
"x-signature": string;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* 生成 HMAC 签名请求头
|
|
235
|
+
*
|
|
236
|
+
* 签名算法:
|
|
237
|
+
* string_to_sign = "${METHOD}\n${path}\n${timestamp}\n${nonce}\n${SHA256(body)}"
|
|
238
|
+
* signature = HMAC_SHA256(appSecret, string_to_sign).hex()
|
|
239
|
+
*
|
|
240
|
+
* 服务端验证时:
|
|
241
|
+
* - 时间窗口 ±300 秒
|
|
242
|
+
* - Nonce 去重(Redis SET NX,TTL 600 秒)
|
|
243
|
+
* - timingSafeEqual 比较签名
|
|
244
|
+
*/
|
|
245
|
+
declare function signRequest(params: {
|
|
246
|
+
method: string;
|
|
247
|
+
path: string;
|
|
248
|
+
body: string;
|
|
249
|
+
appId: string;
|
|
250
|
+
appSecret: string;
|
|
251
|
+
}): SignedHeaders;
|
|
252
|
+
/**
|
|
253
|
+
* 生成 identity hash
|
|
254
|
+
*
|
|
255
|
+
* 服务端期望的 identity_hash = HMAC-SHA256(appSecret, identity).hex()
|
|
256
|
+
*/
|
|
257
|
+
declare function hashIdentity(identity: string, appSecret: string): string;
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* 生成设备机器码
|
|
261
|
+
*
|
|
262
|
+
* 取硬件特征的 HMAC-SHA256 哈希,格式化为 MC-XXXX-XXXX-XXXX-XXXX
|
|
263
|
+
* 同一台机器多次调用结果一致(确定性)
|
|
264
|
+
*/
|
|
265
|
+
declare function getMachineCode(appSecret?: string): string;
|
|
266
|
+
/**
|
|
267
|
+
* 获取设备信息
|
|
268
|
+
*/
|
|
269
|
+
declare function getDeviceInfo(): DeviceInfo;
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* 文件存储
|
|
273
|
+
*
|
|
274
|
+
* 缓存内容用 AES-256-GCM 加密,密钥由 identity 派生。
|
|
275
|
+
* 换设备(identity 不同)后旧缓存自动失效。
|
|
276
|
+
*
|
|
277
|
+
* 文件格式:iv(12) + tag(16) + ciphertext
|
|
278
|
+
*/
|
|
279
|
+
declare class FileStorage {
|
|
280
|
+
private filePath;
|
|
281
|
+
private encryptionKey;
|
|
282
|
+
constructor(storagePath: string, appId: string);
|
|
283
|
+
/** 保存缓存数据(AES-256-GCM 加密) */
|
|
284
|
+
save(data: CacheData): void;
|
|
285
|
+
/** 读取缓存数据(解密) */
|
|
286
|
+
load(): CacheData | null;
|
|
287
|
+
/** 清除缓存文件 */
|
|
288
|
+
clear(): void;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export { type ActivateResult, type ApiResponse, type CacheData, Clavis, type ClavisConfig, ClavisError, type DeviceInfo, type ErrorCode, ErrorCodes, FileStorage, type HeartbeatResult, type LicenseStatus, type PricingPlan, type TokenPayload, Clavis as default, getDeviceInfo, getMachineCode, hashIdentity, signRequest, verifyOfflineToken };
|