@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/src/crypto.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Ed25519 离线验签
|
|
3
|
+
// 与服务端 src/lib/crypto.ts 的 signPayload/verifyToken 完全对齐
|
|
4
|
+
// ============================================================
|
|
5
|
+
|
|
6
|
+
import nacl from "tweetnacl";
|
|
7
|
+
import { decodeBase64, encodeUTF8 } from "tweetnacl-util";
|
|
8
|
+
import type { TokenPayload } from "./types";
|
|
9
|
+
|
|
10
|
+
// ---- base64url 辅助 ----
|
|
11
|
+
|
|
12
|
+
function fromBase64Url(b64url: string): string {
|
|
13
|
+
let b64 = b64url.replace(/-/g, "+").replace(/_/g, "/");
|
|
14
|
+
while (b64.length % 4 !== 0) b64 += "=";
|
|
15
|
+
return b64;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 验证 offline token 的 Ed25519 签名
|
|
20
|
+
*
|
|
21
|
+
* token 格式: base64url(payload_json).base64url(signature_64bytes)
|
|
22
|
+
*
|
|
23
|
+
* 签名流程(服务端 signPayload):
|
|
24
|
+
* 1. payloadJson = JSON.stringify(payload)
|
|
25
|
+
* 2. messageBytes = TextEncoder.encode(payloadJson)
|
|
26
|
+
* 3. signedMessage = nacl.sign(messageBytes, privateKey) → 64字节签名 + 消息
|
|
27
|
+
* 4. signature = signedMessage.slice(0, 64)
|
|
28
|
+
* 5. token = base64url(payloadJson) + "." + base64url(signature)
|
|
29
|
+
*
|
|
30
|
+
* 验签流程(本函数):
|
|
31
|
+
* 1. 从 token 拆出 payloadBase64Url 和 signatureBase64Url
|
|
32
|
+
* 2. payloadJson = base64url_decode(payloadBase64Url) → UTF-8 字符串
|
|
33
|
+
* 3. messageBytes = TextEncoder.encode(payloadJson)
|
|
34
|
+
* 4. signatureBytes = base64url_decode(signatureBase64Url) → 64 字节
|
|
35
|
+
* 5. 构造 signedMessage = signature + message
|
|
36
|
+
* 6. nacl.sign.open(signedMessage, publicKey) → 验证成功则返回 message
|
|
37
|
+
* 7. 解析 payloadJson 为 TokenPayload
|
|
38
|
+
*/
|
|
39
|
+
export function verifyOfflineToken(
|
|
40
|
+
token: string,
|
|
41
|
+
publicKeyBase64: string
|
|
42
|
+
): TokenPayload | null {
|
|
43
|
+
try {
|
|
44
|
+
const parts = token.split(".");
|
|
45
|
+
if (parts.length !== 2) return null;
|
|
46
|
+
|
|
47
|
+
const [payloadBase64Url, signatureBase64Url] = parts;
|
|
48
|
+
|
|
49
|
+
// 1. 解码 payload JSON 字符串
|
|
50
|
+
// 服务端用 Buffer.from(payloadJson).toString("base64") 再转 base64url
|
|
51
|
+
// 所以这里逆向:base64url → base64 → Buffer → UTF-8
|
|
52
|
+
const payloadB64 = fromBase64Url(payloadBase64Url);
|
|
53
|
+
const payloadBytes = Uint8Array.from(atob(payloadB64), (c) =>
|
|
54
|
+
c.charCodeAt(0)
|
|
55
|
+
);
|
|
56
|
+
const payloadJson = new TextDecoder().decode(payloadBytes);
|
|
57
|
+
|
|
58
|
+
// 2. 解码签名(64 字节)
|
|
59
|
+
const signatureB64 = fromBase64Url(signatureBase64Url);
|
|
60
|
+
const signatureBytes = decodeBase64(signatureB64);
|
|
61
|
+
|
|
62
|
+
// 3. 解码公钥
|
|
63
|
+
const publicKeyBytes = decodeBase64(publicKeyBase64);
|
|
64
|
+
|
|
65
|
+
// 4. 重新 encode payloadJson 为字节(与签名时一致)
|
|
66
|
+
const messageBytes = new TextEncoder().encode(payloadJson);
|
|
67
|
+
|
|
68
|
+
// 5. 构造 signedMessage = signature(64) + message
|
|
69
|
+
const signedMessage = new Uint8Array(
|
|
70
|
+
signatureBytes.length + messageBytes.length
|
|
71
|
+
);
|
|
72
|
+
signedMessage.set(signatureBytes);
|
|
73
|
+
signedMessage.set(messageBytes, signatureBytes.length);
|
|
74
|
+
|
|
75
|
+
// 6. 验签
|
|
76
|
+
const verified = nacl.sign.open(signedMessage, publicKeyBytes);
|
|
77
|
+
if (!verified) return null;
|
|
78
|
+
|
|
79
|
+
// 7. 解析 payload
|
|
80
|
+
return JSON.parse(payloadJson) as TokenPayload;
|
|
81
|
+
} catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Clavis SDK 错误码
|
|
3
|
+
// ============================================================
|
|
4
|
+
|
|
5
|
+
export class ClavisError extends Error {
|
|
6
|
+
code: string;
|
|
7
|
+
|
|
8
|
+
constructor(code: string, message: string) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "ClavisError";
|
|
11
|
+
this.code = code;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** 错误码枚举 */
|
|
16
|
+
export const ErrorCodes = {
|
|
17
|
+
// 网络
|
|
18
|
+
NETWORK_ERROR: "NETWORK_ERROR",
|
|
19
|
+
API_ERROR: "API_ERROR",
|
|
20
|
+
|
|
21
|
+
// 签名
|
|
22
|
+
INVALID_SIGNATURE: "INVALID_SIGNATURE",
|
|
23
|
+
MISSING_AUTH_HEADERS: "MISSING_AUTH_HEADERS",
|
|
24
|
+
TIMESTAMP_EXPIRED: "TIMESTAMP_EXPIRED",
|
|
25
|
+
|
|
26
|
+
// 授权码
|
|
27
|
+
LICENSE_NOT_FOUND: "LICENSE_NOT_FOUND",
|
|
28
|
+
LICENSE_EXPIRED: "LICENSE_EXPIRED",
|
|
29
|
+
LICENSE_REVOKED: "LICENSE_REVOKED",
|
|
30
|
+
ACTIVATION_LIMIT: "ACTIVATION_LIMIT",
|
|
31
|
+
ACTIVATION_NOT_FOUND: "ACTIVATION_NOT_FOUND",
|
|
32
|
+
|
|
33
|
+
// 离线验证
|
|
34
|
+
NO_CACHED_TOKEN: "NO_CACHED_TOKEN",
|
|
35
|
+
TOKEN_SIGNATURE_INVALID: "TOKEN_SIGNATURE_INVALID",
|
|
36
|
+
TOKEN_EXPIRED: "TOKEN_EXPIRED",
|
|
37
|
+
TOKEN_GRACE_EXPIRED: "TOKEN_GRACE_EXPIRED",
|
|
38
|
+
IDENTITY_MISMATCH: "IDENTITY_MISMATCH",
|
|
39
|
+
CLOCK_DRIFT_DETECTED: "CLOCK_DRIFT_DETECTED",
|
|
40
|
+
|
|
41
|
+
// 配置
|
|
42
|
+
MISSING_CONFIG: "MISSING_CONFIG",
|
|
43
|
+
INVALID_CONFIG: "INVALID_CONFIG",
|
|
44
|
+
} as const;
|
|
45
|
+
|
|
46
|
+
export type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes];
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// 设备指纹生成
|
|
3
|
+
// ============================================================
|
|
4
|
+
|
|
5
|
+
import crypto from "crypto";
|
|
6
|
+
import os from "os";
|
|
7
|
+
import type { DeviceInfo } from "./types";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 生成设备机器码
|
|
11
|
+
*
|
|
12
|
+
* 取硬件特征的 HMAC-SHA256 哈希,格式化为 MC-XXXX-XXXX-XXXX-XXXX
|
|
13
|
+
* 同一台机器多次调用结果一致(确定性)
|
|
14
|
+
*/
|
|
15
|
+
export function getMachineCode(appSecret?: string): string {
|
|
16
|
+
const cpus = os.cpus();
|
|
17
|
+
const data = [
|
|
18
|
+
os.hostname(),
|
|
19
|
+
os.platform(),
|
|
20
|
+
os.arch(),
|
|
21
|
+
cpus[0]?.model || "",
|
|
22
|
+
cpus.length.toString(),
|
|
23
|
+
os.totalmem().toString(),
|
|
24
|
+
].join("|");
|
|
25
|
+
|
|
26
|
+
const key = appSecret || "clavis-default-key";
|
|
27
|
+
const hash = crypto.createHmac("sha256", key).update(data).digest("hex");
|
|
28
|
+
|
|
29
|
+
// 格式化为 MC-XXXX-XXXX-XXXX-XXXX
|
|
30
|
+
return `MC-${hash.substring(0, 4).toUpperCase()}-${hash.substring(4, 8).toUpperCase()}-${hash.substring(8, 12).toUpperCase()}-${hash.substring(12, 16).toUpperCase()}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 获取设备信息
|
|
35
|
+
*/
|
|
36
|
+
export function getDeviceInfo(): DeviceInfo {
|
|
37
|
+
const cpus = os.cpus();
|
|
38
|
+
return {
|
|
39
|
+
os: os.platform(),
|
|
40
|
+
osVersion: os.release(),
|
|
41
|
+
hostname: os.hostname(),
|
|
42
|
+
arch: os.arch(),
|
|
43
|
+
cpuModel: cpus[0]?.model || "unknown",
|
|
44
|
+
totalMemory: os.totalmem(),
|
|
45
|
+
};
|
|
46
|
+
}
|
package/src/hmac.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// HMAC-SHA256 请求签名
|
|
3
|
+
// 与服务端 src/lib/api-auth.ts 的 validateHmacSignature 完全对齐
|
|
4
|
+
// ============================================================
|
|
5
|
+
|
|
6
|
+
import crypto from "crypto";
|
|
7
|
+
|
|
8
|
+
export interface SignedHeaders {
|
|
9
|
+
"x-app-id": string;
|
|
10
|
+
"x-timestamp": string;
|
|
11
|
+
"x-nonce": string;
|
|
12
|
+
"x-signature": string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 生成 HMAC 签名请求头
|
|
17
|
+
*
|
|
18
|
+
* 签名算法:
|
|
19
|
+
* string_to_sign = "${METHOD}\n${path}\n${timestamp}\n${nonce}\n${SHA256(body)}"
|
|
20
|
+
* signature = HMAC_SHA256(appSecret, string_to_sign).hex()
|
|
21
|
+
*
|
|
22
|
+
* 服务端验证时:
|
|
23
|
+
* - 时间窗口 ±300 秒
|
|
24
|
+
* - Nonce 去重(Redis SET NX,TTL 600 秒)
|
|
25
|
+
* - timingSafeEqual 比较签名
|
|
26
|
+
*/
|
|
27
|
+
export function signRequest(params: {
|
|
28
|
+
method: string;
|
|
29
|
+
path: string;
|
|
30
|
+
body: string;
|
|
31
|
+
appId: string;
|
|
32
|
+
appSecret: string;
|
|
33
|
+
}): SignedHeaders {
|
|
34
|
+
const timestamp = Math.floor(Date.now() / 1000).toString();
|
|
35
|
+
const nonce = crypto.randomUUID();
|
|
36
|
+
|
|
37
|
+
const bodyHash = crypto
|
|
38
|
+
.createHash("sha256")
|
|
39
|
+
.update(params.body || "")
|
|
40
|
+
.digest("hex");
|
|
41
|
+
|
|
42
|
+
const stringToSign = `${params.method.toUpperCase()}\n${params.path}\n${timestamp}\n${nonce}\n${bodyHash}`;
|
|
43
|
+
|
|
44
|
+
const signature = crypto
|
|
45
|
+
.createHmac("sha256", params.appSecret)
|
|
46
|
+
.update(stringToSign)
|
|
47
|
+
.digest("hex");
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
"x-app-id": params.appId,
|
|
51
|
+
"x-timestamp": timestamp,
|
|
52
|
+
"x-nonce": nonce,
|
|
53
|
+
"x-signature": signature,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 生成 identity hash
|
|
59
|
+
*
|
|
60
|
+
* 服务端期望的 identity_hash = HMAC-SHA256(appSecret, identity).hex()
|
|
61
|
+
*/
|
|
62
|
+
export function hashIdentity(identity: string, appSecret: string): string {
|
|
63
|
+
return crypto
|
|
64
|
+
.createHmac("sha256", appSecret)
|
|
65
|
+
.update(identity)
|
|
66
|
+
.digest("hex");
|
|
67
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// @nietzsci/clavis 主入口
|
|
3
|
+
// ============================================================
|
|
4
|
+
|
|
5
|
+
// 主类(默认导出)
|
|
6
|
+
import { Clavis } from "./client";
|
|
7
|
+
export default Clavis;
|
|
8
|
+
|
|
9
|
+
// 同时提供命名导出(兼容两种写法)
|
|
10
|
+
export { Clavis } from "./client";
|
|
11
|
+
|
|
12
|
+
// 类型
|
|
13
|
+
export type {
|
|
14
|
+
ClavisConfig,
|
|
15
|
+
ActivateResult,
|
|
16
|
+
LicenseStatus,
|
|
17
|
+
TokenPayload,
|
|
18
|
+
HeartbeatResult,
|
|
19
|
+
PricingPlan,
|
|
20
|
+
DeviceInfo,
|
|
21
|
+
CacheData,
|
|
22
|
+
ApiResponse,
|
|
23
|
+
} from "./types";
|
|
24
|
+
|
|
25
|
+
// 错误
|
|
26
|
+
export { ClavisError, ErrorCodes } from "./errors";
|
|
27
|
+
export type { ErrorCode } from "./errors";
|
|
28
|
+
|
|
29
|
+
// 工具函数
|
|
30
|
+
export { verifyOfflineToken } from "./crypto";
|
|
31
|
+
export { signRequest, hashIdentity } from "./hmac";
|
|
32
|
+
export { getMachineCode, getDeviceInfo } from "./fingerprint";
|
|
33
|
+
export { FileStorage } from "./storage";
|
package/src/storage.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// 本地缓存管理(AES-256-GCM 加密)
|
|
3
|
+
// ============================================================
|
|
4
|
+
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import crypto from "crypto";
|
|
8
|
+
import type { CacheData } from "./types";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 文件存储
|
|
12
|
+
*
|
|
13
|
+
* 缓存内容用 AES-256-GCM 加密,密钥由 identity 派生。
|
|
14
|
+
* 换设备(identity 不同)后旧缓存自动失效。
|
|
15
|
+
*
|
|
16
|
+
* 文件格式:iv(12) + tag(16) + ciphertext
|
|
17
|
+
*/
|
|
18
|
+
export class FileStorage {
|
|
19
|
+
private filePath: string;
|
|
20
|
+
private encryptionKey: Buffer;
|
|
21
|
+
|
|
22
|
+
constructor(storagePath: string, appId: string) {
|
|
23
|
+
// 缓存文件以 appId 区分,避免多应用冲突
|
|
24
|
+
const safeName = appId.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
25
|
+
this.filePath = path.join(storagePath, `.clavis_${safeName}`);
|
|
26
|
+
// 密钥由 appId 派生(同一应用同一路径 = 同一密钥)
|
|
27
|
+
this.encryptionKey = crypto.createHash("sha256").update(appId).digest();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 保存缓存数据(AES-256-GCM 加密) */
|
|
31
|
+
save(data: CacheData): void {
|
|
32
|
+
const json = JSON.stringify(data);
|
|
33
|
+
const iv = crypto.randomBytes(12);
|
|
34
|
+
const cipher = crypto.createCipheriv("aes-256-gcm", this.encryptionKey, iv);
|
|
35
|
+
const encrypted = Buffer.concat([
|
|
36
|
+
cipher.update(json, "utf8"),
|
|
37
|
+
cipher.final(),
|
|
38
|
+
]);
|
|
39
|
+
const tag = cipher.getAuthTag();
|
|
40
|
+
const combined = Buffer.concat([iv, tag, encrypted]);
|
|
41
|
+
|
|
42
|
+
// 确保目录存在
|
|
43
|
+
const dir = path.dirname(this.filePath);
|
|
44
|
+
if (!fs.existsSync(dir)) {
|
|
45
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
fs.writeFileSync(this.filePath, combined);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** 读取缓存数据(解密) */
|
|
52
|
+
load(): CacheData | null {
|
|
53
|
+
try {
|
|
54
|
+
if (!fs.existsSync(this.filePath)) return null;
|
|
55
|
+
|
|
56
|
+
const data = fs.readFileSync(this.filePath);
|
|
57
|
+
if (data.length < 28) return null; // iv(12) + tag(16) = 28 最小
|
|
58
|
+
|
|
59
|
+
const iv = data.subarray(0, 12);
|
|
60
|
+
const tag = data.subarray(12, 28);
|
|
61
|
+
const encrypted = data.subarray(28);
|
|
62
|
+
|
|
63
|
+
const decipher = crypto.createDecipheriv(
|
|
64
|
+
"aes-256-gcm",
|
|
65
|
+
this.encryptionKey,
|
|
66
|
+
iv
|
|
67
|
+
);
|
|
68
|
+
decipher.setAuthTag(tag);
|
|
69
|
+
|
|
70
|
+
const decrypted =
|
|
71
|
+
decipher.update(encrypted, undefined, "utf8") +
|
|
72
|
+
decipher.final("utf8");
|
|
73
|
+
|
|
74
|
+
return JSON.parse(decrypted) as CacheData;
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** 清除缓存文件 */
|
|
81
|
+
clear(): void {
|
|
82
|
+
try {
|
|
83
|
+
if (fs.existsSync(this.filePath)) {
|
|
84
|
+
fs.unlinkSync(this.filePath);
|
|
85
|
+
}
|
|
86
|
+
} catch {
|
|
87
|
+
// 忽略删除失败
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Clavis SDK 类型定义
|
|
3
|
+
// ============================================================
|
|
4
|
+
|
|
5
|
+
/** SDK 初始化配置 */
|
|
6
|
+
export interface ClavisConfig {
|
|
7
|
+
/** 应用 ID,格式 nc_xxx */
|
|
8
|
+
appId: string;
|
|
9
|
+
/** 应用密钥,用于 HMAC 签名(服务端 SDK 使用,纯客户端不传) */
|
|
10
|
+
appSecret: string;
|
|
11
|
+
/** Ed25519 公钥(标准 base64),用于离线验证 */
|
|
12
|
+
publicKey: string;
|
|
13
|
+
/** API 地址,默认 https://c.nietzsci.com */
|
|
14
|
+
baseUrl?: string;
|
|
15
|
+
/** 本地缓存路径(Node.js 环境),默认 process.cwd() */
|
|
16
|
+
storagePath?: string;
|
|
17
|
+
/** 离线宽限天数,默认 7 */
|
|
18
|
+
offlineGraceDays?: number;
|
|
19
|
+
/** 心跳间隔(秒),默认 3600 */
|
|
20
|
+
heartbeatInterval?: number;
|
|
21
|
+
/** 是否自动心跳,默认 true */
|
|
22
|
+
autoHeartbeat?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** 配置(所有字段已填充默认值) */
|
|
26
|
+
export interface ClavisConfigResolved {
|
|
27
|
+
appId: string;
|
|
28
|
+
appSecret: string;
|
|
29
|
+
publicKey: string;
|
|
30
|
+
baseUrl: string;
|
|
31
|
+
storagePath: string;
|
|
32
|
+
offlineGraceDays: number;
|
|
33
|
+
heartbeatInterval: number;
|
|
34
|
+
autoHeartbeat: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 激活结果 */
|
|
38
|
+
export interface ActivateResult {
|
|
39
|
+
success: boolean;
|
|
40
|
+
tier: string;
|
|
41
|
+
features: string[];
|
|
42
|
+
expiresAt: string | null;
|
|
43
|
+
offlineToken: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** 授权状态 */
|
|
47
|
+
export interface LicenseStatus {
|
|
48
|
+
valid: boolean;
|
|
49
|
+
tier: string;
|
|
50
|
+
features: string[];
|
|
51
|
+
expiresAt: string | null;
|
|
52
|
+
daysRemaining: number | null;
|
|
53
|
+
isOffline: boolean;
|
|
54
|
+
isTrial: boolean;
|
|
55
|
+
inGracePeriod: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 离线 Token Payload(与服务端 signPayload 的 payload 完全一致) */
|
|
59
|
+
export interface TokenPayload {
|
|
60
|
+
app_id: string;
|
|
61
|
+
license_id: string;
|
|
62
|
+
identity_hash: string;
|
|
63
|
+
tier: string;
|
|
64
|
+
features: string[];
|
|
65
|
+
issued_at: number;
|
|
66
|
+
expires_at: number | null;
|
|
67
|
+
grace_until: number;
|
|
68
|
+
token_version: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** 心跳结果 */
|
|
72
|
+
export interface HeartbeatResult {
|
|
73
|
+
serverTimestamp: number;
|
|
74
|
+
status: string;
|
|
75
|
+
offlineToken?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** 定价方案 */
|
|
79
|
+
export interface PricingPlan {
|
|
80
|
+
id: string;
|
|
81
|
+
tier: string;
|
|
82
|
+
name: string;
|
|
83
|
+
duration: string;
|
|
84
|
+
durationDays: number;
|
|
85
|
+
price: number;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** 设备信息 */
|
|
89
|
+
export interface DeviceInfo {
|
|
90
|
+
os: string;
|
|
91
|
+
osVersion: string;
|
|
92
|
+
hostname: string;
|
|
93
|
+
arch: string;
|
|
94
|
+
cpuModel: string;
|
|
95
|
+
totalMemory: number;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** 本地缓存数据结构 */
|
|
99
|
+
export interface CacheData {
|
|
100
|
+
offlineToken: string;
|
|
101
|
+
lastVerifiedAt: number;
|
|
102
|
+
tokenVersion: number;
|
|
103
|
+
licenseKey: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** API 响应通用结构 */
|
|
107
|
+
export interface ApiResponse<T = unknown> {
|
|
108
|
+
success: boolean;
|
|
109
|
+
data?: T;
|
|
110
|
+
error?: {
|
|
111
|
+
code: string;
|
|
112
|
+
message: string;
|
|
113
|
+
};
|
|
114
|
+
}
|