@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/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
+ }