@honor-claw/yoyo 1.1.2 → 1.1.4-beta.1
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/index.ts +25 -25
- package/package.json +5 -5
- package/src/apis/claw-cloud.ts +124 -124
- package/src/apis/helpers.ts +10 -10
- package/src/apis/honor-auth.ts +158 -158
- package/src/apis/http-client.ts +239 -239
- package/src/apis/index.ts +8 -8
- package/src/apis/types.ts +77 -73
- package/src/cloud-channel/channel.ts +117 -117
- package/src/cloud-channel/client.ts +3 -3
- package/src/cloud-channel/index.ts +4 -4
- package/src/cloud-channel/message-handler.ts +50 -42
- package/src/cloud-channel/session-manager.ts +14 -9
- package/src/cloud-channel/types.ts +115 -115
- package/src/commands/env/impl.ts +58 -58
- package/src/commands/env/index.ts +1 -1
- package/src/commands/index.ts +30 -30
- package/src/commands/login/impl.ts +30 -30
- package/src/commands/login/index.ts +1 -1
- package/src/commands/logout/index.ts +1 -1
- package/src/commands/status/index.ts +194 -194
- package/src/gateway-client/client.ts +90 -90
- package/src/gateway-client/device/auth.ts +57 -57
- package/src/gateway-client/device/builder.ts +105 -105
- package/src/gateway-client/device/helpers.ts +40 -40
- package/src/gateway-client/device/identity.ts +251 -251
- package/src/gateway-client/device/index.ts +40 -40
- package/src/gateway-client/device/types.ts +57 -57
- package/src/gateway-client/index.ts +5 -5
- package/src/gateway-client/protocol-client.ts +49 -34
- package/src/honor-auth/browser.ts +2 -2
- package/src/honor-auth/callback-server.ts +109 -109
- package/src/honor-auth/cloud.ts +57 -57
- package/src/honor-auth/config.ts +43 -43
- package/src/honor-auth/index.ts +3 -3
- package/src/honor-auth/token-manager.ts +90 -90
- package/src/honor-auth/types.ts +50 -50
- package/src/index.ts +10 -10
- package/src/modules/claw-configs/config-manager.ts +409 -409
- package/src/modules/claw-configs/hosts.ts +48 -48
- package/src/modules/claw-configs/index.ts +8 -8
- package/src/modules/claw-configs/provider.ts +394 -394
- package/src/modules/claw-configs/types.ts +34 -34
- package/src/modules/device/device-info.ts +1 -1
- package/src/modules/device/index.ts +3 -3
- package/src/modules/device/providers/base.ts +32 -32
- package/src/modules/device/providers/pad.ts +107 -107
- package/src/modules/device/providers/windows.ts +130 -130
- package/src/modules/device/registry.ts +48 -43
- package/src/modules/login/impl.ts +31 -26
- package/src/modules/login/index.ts +6 -6
- package/src/schemas.ts +23 -23
- package/src/services/connection/impl.ts +339 -339
- package/src/services/connection/status-tracker/events.ts +127 -127
- package/src/services/connection/status-tracker/index.ts +31 -31
- package/src/services/connection/status-tracker/storage.ts +133 -133
- package/src/services/connection/status-tracker/tracker.ts +370 -370
- package/src/services/connection/status-tracker/types.ts +131 -131
- package/src/services/connection/types.ts +20 -20
- package/src/types.ts +64 -64
- package/src/utils/id.ts +8 -8
- package/src/utils/jwt.ts +37 -37
- package/src/utils/logger.ts +20 -20
- package/src/utils/proxy.ts +58 -58
- package/src/utils/version.ts +29 -29
- package/src/utils/ws.ts +21 -21
|
@@ -1,251 +1,251 @@
|
|
|
1
|
-
import crypto from "node:crypto";
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import os from "node:os";
|
|
5
|
-
import type { DeviceIdentity } from "./types.js";
|
|
6
|
-
|
|
7
|
-
type StoredIdentity = {
|
|
8
|
-
version: 1;
|
|
9
|
-
deviceId: string;
|
|
10
|
-
publicKeyPem: string;
|
|
11
|
-
privateKeyPem: string;
|
|
12
|
-
createdAtMs: number;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Resolve default identity path
|
|
17
|
-
* Uses OS-specific state directory
|
|
18
|
-
*/
|
|
19
|
-
function resolveDefaultIdentityPath(): string {
|
|
20
|
-
const platform = process.platform;
|
|
21
|
-
let stateDir: string;
|
|
22
|
-
|
|
23
|
-
if (platform === "darwin") {
|
|
24
|
-
stateDir = path.join(os.homedir(), "Library", "Application Support", "xh-gateway-client");
|
|
25
|
-
} else if (platform === "win32") {
|
|
26
|
-
stateDir = path.join(os.homedir(), "AppData", "Local", "xh-gateway-client");
|
|
27
|
-
} else {
|
|
28
|
-
// Linux and others
|
|
29
|
-
stateDir = process.env.XDG_STATE_HOME
|
|
30
|
-
? path.join(process.env.XDG_STATE_HOME, "xh-gateway-client")
|
|
31
|
-
: path.join(os.homedir(), ".local", "state", "xh-gateway-client");
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return path.join(stateDir, "identity", "device.json");
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Ensure directory exists
|
|
39
|
-
*/
|
|
40
|
-
function ensureDir(filePath: string): void {
|
|
41
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* ED25519 SPKI prefix for public key extraction
|
|
46
|
-
*/
|
|
47
|
-
const ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Base64 URL encode a buffer
|
|
51
|
-
*/
|
|
52
|
-
function base64UrlEncode(buf: Buffer): string {
|
|
53
|
-
return buf.toString("base64").replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/g, "");
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Base64 URL decode a string
|
|
58
|
-
*/
|
|
59
|
-
function base64UrlDecode(input: string): Buffer {
|
|
60
|
-
const normalized = input.replaceAll("-", "+").replaceAll("_", "/");
|
|
61
|
-
const padded = normalized + "=".repeat((4 - (normalized.length % 4)) % 4);
|
|
62
|
-
return Buffer.from(padded, "base64");
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Derive raw public key from PEM format
|
|
67
|
-
*/
|
|
68
|
-
function derivePublicKeyRaw(publicKeyPem: string): Buffer {
|
|
69
|
-
const key = crypto.createPublicKey(publicKeyPem);
|
|
70
|
-
const spki = key.export({ type: "spki", format: "der" }) as Buffer;
|
|
71
|
-
if (
|
|
72
|
-
spki.length === ED25519_SPKI_PREFIX.length + 32 &&
|
|
73
|
-
spki.subarray(0, ED25519_SPKI_PREFIX.length).equals(ED25519_SPKI_PREFIX)
|
|
74
|
-
) {
|
|
75
|
-
return spki.subarray(ED25519_SPKI_PREFIX.length);
|
|
76
|
-
}
|
|
77
|
-
return spki;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Generate fingerprint from public key
|
|
82
|
-
*/
|
|
83
|
-
function fingerprintPublicKey(publicKeyPem: string): string {
|
|
84
|
-
const raw = derivePublicKeyRaw(publicKeyPem);
|
|
85
|
-
return crypto.createHash("sha256").update(raw).digest("hex");
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Generate a new device identity with ED25519 key pair
|
|
90
|
-
*/
|
|
91
|
-
function generateIdentity(): DeviceIdentity {
|
|
92
|
-
const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");
|
|
93
|
-
const publicKeyPem = publicKey.export({ type: "spki", format: "pem" }).toString();
|
|
94
|
-
const privateKeyPem = privateKey.export({ type: "pkcs8", format: "pem" }).toString();
|
|
95
|
-
const deviceId = fingerprintPublicKey(publicKeyPem);
|
|
96
|
-
return { deviceId, publicKeyPem, privateKeyPem };
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Load or create device identity from file
|
|
101
|
-
* @param filePath - Optional path to identity file (defaults to OS-specific location)
|
|
102
|
-
* @returns Device identity
|
|
103
|
-
*/
|
|
104
|
-
export function loadOrCreateDeviceIdentity(
|
|
105
|
-
filePath: string = resolveDefaultIdentityPath(),
|
|
106
|
-
): DeviceIdentity {
|
|
107
|
-
try {
|
|
108
|
-
if (fs.existsSync(filePath)) {
|
|
109
|
-
const raw = fs.readFileSync(filePath, "utf8");
|
|
110
|
-
const parsed = JSON.parse(raw) as StoredIdentity;
|
|
111
|
-
if (
|
|
112
|
-
parsed?.version === 1 &&
|
|
113
|
-
typeof parsed.deviceId === "string" &&
|
|
114
|
-
typeof parsed.publicKeyPem === "string" &&
|
|
115
|
-
typeof parsed.privateKeyPem === "string"
|
|
116
|
-
) {
|
|
117
|
-
const derivedId = fingerprintPublicKey(parsed.publicKeyPem);
|
|
118
|
-
if (derivedId && derivedId !== parsed.deviceId) {
|
|
119
|
-
// Update stored deviceId if it doesn't match fingerprint
|
|
120
|
-
const updated: StoredIdentity = {
|
|
121
|
-
...parsed,
|
|
122
|
-
deviceId: derivedId,
|
|
123
|
-
};
|
|
124
|
-
fs.writeFileSync(filePath, `${JSON.stringify(updated, null, 2)}\n`, { mode: 0o600 });
|
|
125
|
-
try {
|
|
126
|
-
fs.chmodSync(filePath, 0o600);
|
|
127
|
-
} catch {
|
|
128
|
-
// best-effort
|
|
129
|
-
}
|
|
130
|
-
return {
|
|
131
|
-
deviceId: derivedId,
|
|
132
|
-
publicKeyPem: parsed.publicKeyPem,
|
|
133
|
-
privateKeyPem: parsed.privateKeyPem,
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
return {
|
|
137
|
-
deviceId: parsed.deviceId,
|
|
138
|
-
publicKeyPem: parsed.publicKeyPem,
|
|
139
|
-
privateKeyPem: parsed.privateKeyPem,
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
} catch {
|
|
144
|
-
// fall through to regenerate on error
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Generate new identity
|
|
148
|
-
const identity = generateIdentity();
|
|
149
|
-
ensureDir(filePath);
|
|
150
|
-
const stored: StoredIdentity = {
|
|
151
|
-
version: 1,
|
|
152
|
-
deviceId: identity.deviceId,
|
|
153
|
-
publicKeyPem: identity.publicKeyPem,
|
|
154
|
-
privateKeyPem: identity.privateKeyPem,
|
|
155
|
-
createdAtMs: Date.now(),
|
|
156
|
-
};
|
|
157
|
-
fs.writeFileSync(filePath, `${JSON.stringify(stored, null, 2)}\n`, { mode: 0o600 });
|
|
158
|
-
try {
|
|
159
|
-
fs.chmodSync(filePath, 0o600);
|
|
160
|
-
} catch {
|
|
161
|
-
// best-effort
|
|
162
|
-
}
|
|
163
|
-
return identity;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Sign a payload using device private key
|
|
168
|
-
* @param privateKeyPem - Private key in PEM format
|
|
169
|
-
* @param payload - Payload string to sign
|
|
170
|
-
* @returns Base64 URL encoded signature
|
|
171
|
-
*/
|
|
172
|
-
export function signDevicePayload(privateKeyPem: string, payload: string): string {
|
|
173
|
-
const key = crypto.createPrivateKey(privateKeyPem);
|
|
174
|
-
const sig = crypto.sign(null, Buffer.from(payload, "utf8"), key);
|
|
175
|
-
return base64UrlEncode(sig);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Normalize device public key to base64 URL format
|
|
180
|
-
* @param publicKey - Public key in PEM or base64 format
|
|
181
|
-
* @returns Base64 URL encoded public key or null on error
|
|
182
|
-
*/
|
|
183
|
-
export function normalizeDevicePublicKeyBase64Url(publicKey: string): string | null {
|
|
184
|
-
try {
|
|
185
|
-
if (publicKey.includes("BEGIN")) {
|
|
186
|
-
return base64UrlEncode(derivePublicKeyRaw(publicKey));
|
|
187
|
-
}
|
|
188
|
-
const raw = base64UrlDecode(publicKey);
|
|
189
|
-
return base64UrlEncode(raw);
|
|
190
|
-
} catch {
|
|
191
|
-
return null;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Derive device ID from public key
|
|
197
|
-
* @param publicKey - Public key in PEM or base64 format
|
|
198
|
-
* @returns Device ID (SHA256 hash) or null on error
|
|
199
|
-
*/
|
|
200
|
-
export function deriveDeviceIdFromPublicKey(publicKey: string): string | null {
|
|
201
|
-
try {
|
|
202
|
-
const raw = publicKey.includes("BEGIN")
|
|
203
|
-
? derivePublicKeyRaw(publicKey)
|
|
204
|
-
: base64UrlDecode(publicKey);
|
|
205
|
-
return crypto.createHash("sha256").update(raw).digest("hex");
|
|
206
|
-
} catch {
|
|
207
|
-
return null;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* Convert public key PEM to raw base64 URL format
|
|
213
|
-
* @param publicKeyPem - Public key in PEM format
|
|
214
|
-
* @returns Base64 URL encoded raw public key
|
|
215
|
-
*/
|
|
216
|
-
export function publicKeyRawBase64UrlFromPem(publicKeyPem: string): string {
|
|
217
|
-
return base64UrlEncode(derivePublicKeyRaw(publicKeyPem));
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Verify device signature
|
|
222
|
-
* @param publicKey - Public key in PEM or base64 format
|
|
223
|
-
* @param payload - Original payload string
|
|
224
|
-
* @param signatureBase64Url - Base64 URL encoded signature
|
|
225
|
-
* @returns True if signature is valid, false otherwise
|
|
226
|
-
*/
|
|
227
|
-
export function verifyDeviceSignature(
|
|
228
|
-
publicKey: string,
|
|
229
|
-
payload: string,
|
|
230
|
-
signatureBase64Url: string,
|
|
231
|
-
): boolean {
|
|
232
|
-
try {
|
|
233
|
-
const key = publicKey.includes("BEGIN")
|
|
234
|
-
? crypto.createPublicKey(publicKey)
|
|
235
|
-
: crypto.createPublicKey({
|
|
236
|
-
key: Buffer.concat([ED25519_SPKI_PREFIX, base64UrlDecode(publicKey)]),
|
|
237
|
-
type: "spki",
|
|
238
|
-
format: "der",
|
|
239
|
-
});
|
|
240
|
-
const sig = (() => {
|
|
241
|
-
try {
|
|
242
|
-
return base64UrlDecode(signatureBase64Url);
|
|
243
|
-
} catch {
|
|
244
|
-
return Buffer.from(signatureBase64Url, "base64");
|
|
245
|
-
}
|
|
246
|
-
})();
|
|
247
|
-
return crypto.verify(null, Buffer.from(payload, "utf8"), key, sig);
|
|
248
|
-
} catch {
|
|
249
|
-
return false;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import type { DeviceIdentity } from "./types.js";
|
|
6
|
+
|
|
7
|
+
type StoredIdentity = {
|
|
8
|
+
version: 1;
|
|
9
|
+
deviceId: string;
|
|
10
|
+
publicKeyPem: string;
|
|
11
|
+
privateKeyPem: string;
|
|
12
|
+
createdAtMs: number;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Resolve default identity path
|
|
17
|
+
* Uses OS-specific state directory
|
|
18
|
+
*/
|
|
19
|
+
function resolveDefaultIdentityPath(): string {
|
|
20
|
+
const platform = process.platform;
|
|
21
|
+
let stateDir: string;
|
|
22
|
+
|
|
23
|
+
if (platform === "darwin") {
|
|
24
|
+
stateDir = path.join(os.homedir(), "Library", "Application Support", "xh-gateway-client");
|
|
25
|
+
} else if (platform === "win32") {
|
|
26
|
+
stateDir = path.join(os.homedir(), "AppData", "Local", "xh-gateway-client");
|
|
27
|
+
} else {
|
|
28
|
+
// Linux and others
|
|
29
|
+
stateDir = process.env.XDG_STATE_HOME
|
|
30
|
+
? path.join(process.env.XDG_STATE_HOME, "xh-gateway-client")
|
|
31
|
+
: path.join(os.homedir(), ".local", "state", "xh-gateway-client");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return path.join(stateDir, "identity", "device.json");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Ensure directory exists
|
|
39
|
+
*/
|
|
40
|
+
function ensureDir(filePath: string): void {
|
|
41
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* ED25519 SPKI prefix for public key extraction
|
|
46
|
+
*/
|
|
47
|
+
const ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Base64 URL encode a buffer
|
|
51
|
+
*/
|
|
52
|
+
function base64UrlEncode(buf: Buffer): string {
|
|
53
|
+
return buf.toString("base64").replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/g, "");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Base64 URL decode a string
|
|
58
|
+
*/
|
|
59
|
+
function base64UrlDecode(input: string): Buffer {
|
|
60
|
+
const normalized = input.replaceAll("-", "+").replaceAll("_", "/");
|
|
61
|
+
const padded = normalized + "=".repeat((4 - (normalized.length % 4)) % 4);
|
|
62
|
+
return Buffer.from(padded, "base64");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Derive raw public key from PEM format
|
|
67
|
+
*/
|
|
68
|
+
function derivePublicKeyRaw(publicKeyPem: string): Buffer {
|
|
69
|
+
const key = crypto.createPublicKey(publicKeyPem);
|
|
70
|
+
const spki = key.export({ type: "spki", format: "der" }) as Buffer;
|
|
71
|
+
if (
|
|
72
|
+
spki.length === ED25519_SPKI_PREFIX.length + 32 &&
|
|
73
|
+
spki.subarray(0, ED25519_SPKI_PREFIX.length).equals(ED25519_SPKI_PREFIX)
|
|
74
|
+
) {
|
|
75
|
+
return spki.subarray(ED25519_SPKI_PREFIX.length);
|
|
76
|
+
}
|
|
77
|
+
return spki;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Generate fingerprint from public key
|
|
82
|
+
*/
|
|
83
|
+
function fingerprintPublicKey(publicKeyPem: string): string {
|
|
84
|
+
const raw = derivePublicKeyRaw(publicKeyPem);
|
|
85
|
+
return crypto.createHash("sha256").update(raw).digest("hex");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Generate a new device identity with ED25519 key pair
|
|
90
|
+
*/
|
|
91
|
+
function generateIdentity(): DeviceIdentity {
|
|
92
|
+
const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");
|
|
93
|
+
const publicKeyPem = publicKey.export({ type: "spki", format: "pem" }).toString();
|
|
94
|
+
const privateKeyPem = privateKey.export({ type: "pkcs8", format: "pem" }).toString();
|
|
95
|
+
const deviceId = fingerprintPublicKey(publicKeyPem);
|
|
96
|
+
return { deviceId, publicKeyPem, privateKeyPem };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Load or create device identity from file
|
|
101
|
+
* @param filePath - Optional path to identity file (defaults to OS-specific location)
|
|
102
|
+
* @returns Device identity
|
|
103
|
+
*/
|
|
104
|
+
export function loadOrCreateDeviceIdentity(
|
|
105
|
+
filePath: string = resolveDefaultIdentityPath(),
|
|
106
|
+
): DeviceIdentity {
|
|
107
|
+
try {
|
|
108
|
+
if (fs.existsSync(filePath)) {
|
|
109
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
110
|
+
const parsed = JSON.parse(raw) as StoredIdentity;
|
|
111
|
+
if (
|
|
112
|
+
parsed?.version === 1 &&
|
|
113
|
+
typeof parsed.deviceId === "string" &&
|
|
114
|
+
typeof parsed.publicKeyPem === "string" &&
|
|
115
|
+
typeof parsed.privateKeyPem === "string"
|
|
116
|
+
) {
|
|
117
|
+
const derivedId = fingerprintPublicKey(parsed.publicKeyPem);
|
|
118
|
+
if (derivedId && derivedId !== parsed.deviceId) {
|
|
119
|
+
// Update stored deviceId if it doesn't match fingerprint
|
|
120
|
+
const updated: StoredIdentity = {
|
|
121
|
+
...parsed,
|
|
122
|
+
deviceId: derivedId,
|
|
123
|
+
};
|
|
124
|
+
fs.writeFileSync(filePath, `${JSON.stringify(updated, null, 2)}\n`, { mode: 0o600 });
|
|
125
|
+
try {
|
|
126
|
+
fs.chmodSync(filePath, 0o600);
|
|
127
|
+
} catch {
|
|
128
|
+
// best-effort
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
deviceId: derivedId,
|
|
132
|
+
publicKeyPem: parsed.publicKeyPem,
|
|
133
|
+
privateKeyPem: parsed.privateKeyPem,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
deviceId: parsed.deviceId,
|
|
138
|
+
publicKeyPem: parsed.publicKeyPem,
|
|
139
|
+
privateKeyPem: parsed.privateKeyPem,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
} catch {
|
|
144
|
+
// fall through to regenerate on error
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Generate new identity
|
|
148
|
+
const identity = generateIdentity();
|
|
149
|
+
ensureDir(filePath);
|
|
150
|
+
const stored: StoredIdentity = {
|
|
151
|
+
version: 1,
|
|
152
|
+
deviceId: identity.deviceId,
|
|
153
|
+
publicKeyPem: identity.publicKeyPem,
|
|
154
|
+
privateKeyPem: identity.privateKeyPem,
|
|
155
|
+
createdAtMs: Date.now(),
|
|
156
|
+
};
|
|
157
|
+
fs.writeFileSync(filePath, `${JSON.stringify(stored, null, 2)}\n`, { mode: 0o600 });
|
|
158
|
+
try {
|
|
159
|
+
fs.chmodSync(filePath, 0o600);
|
|
160
|
+
} catch {
|
|
161
|
+
// best-effort
|
|
162
|
+
}
|
|
163
|
+
return identity;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Sign a payload using device private key
|
|
168
|
+
* @param privateKeyPem - Private key in PEM format
|
|
169
|
+
* @param payload - Payload string to sign
|
|
170
|
+
* @returns Base64 URL encoded signature
|
|
171
|
+
*/
|
|
172
|
+
export function signDevicePayload(privateKeyPem: string, payload: string): string {
|
|
173
|
+
const key = crypto.createPrivateKey(privateKeyPem);
|
|
174
|
+
const sig = crypto.sign(null, Buffer.from(payload, "utf8"), key);
|
|
175
|
+
return base64UrlEncode(sig);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Normalize device public key to base64 URL format
|
|
180
|
+
* @param publicKey - Public key in PEM or base64 format
|
|
181
|
+
* @returns Base64 URL encoded public key or null on error
|
|
182
|
+
*/
|
|
183
|
+
export function normalizeDevicePublicKeyBase64Url(publicKey: string): string | null {
|
|
184
|
+
try {
|
|
185
|
+
if (publicKey.includes("BEGIN")) {
|
|
186
|
+
return base64UrlEncode(derivePublicKeyRaw(publicKey));
|
|
187
|
+
}
|
|
188
|
+
const raw = base64UrlDecode(publicKey);
|
|
189
|
+
return base64UrlEncode(raw);
|
|
190
|
+
} catch {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Derive device ID from public key
|
|
197
|
+
* @param publicKey - Public key in PEM or base64 format
|
|
198
|
+
* @returns Device ID (SHA256 hash) or null on error
|
|
199
|
+
*/
|
|
200
|
+
export function deriveDeviceIdFromPublicKey(publicKey: string): string | null {
|
|
201
|
+
try {
|
|
202
|
+
const raw = publicKey.includes("BEGIN")
|
|
203
|
+
? derivePublicKeyRaw(publicKey)
|
|
204
|
+
: base64UrlDecode(publicKey);
|
|
205
|
+
return crypto.createHash("sha256").update(raw).digest("hex");
|
|
206
|
+
} catch {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Convert public key PEM to raw base64 URL format
|
|
213
|
+
* @param publicKeyPem - Public key in PEM format
|
|
214
|
+
* @returns Base64 URL encoded raw public key
|
|
215
|
+
*/
|
|
216
|
+
export function publicKeyRawBase64UrlFromPem(publicKeyPem: string): string {
|
|
217
|
+
return base64UrlEncode(derivePublicKeyRaw(publicKeyPem));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Verify device signature
|
|
222
|
+
* @param publicKey - Public key in PEM or base64 format
|
|
223
|
+
* @param payload - Original payload string
|
|
224
|
+
* @param signatureBase64Url - Base64 URL encoded signature
|
|
225
|
+
* @returns True if signature is valid, false otherwise
|
|
226
|
+
*/
|
|
227
|
+
export function verifyDeviceSignature(
|
|
228
|
+
publicKey: string,
|
|
229
|
+
payload: string,
|
|
230
|
+
signatureBase64Url: string,
|
|
231
|
+
): boolean {
|
|
232
|
+
try {
|
|
233
|
+
const key = publicKey.includes("BEGIN")
|
|
234
|
+
? crypto.createPublicKey(publicKey)
|
|
235
|
+
: crypto.createPublicKey({
|
|
236
|
+
key: Buffer.concat([ED25519_SPKI_PREFIX, base64UrlDecode(publicKey)]),
|
|
237
|
+
type: "spki",
|
|
238
|
+
format: "der",
|
|
239
|
+
});
|
|
240
|
+
const sig = (() => {
|
|
241
|
+
try {
|
|
242
|
+
return base64UrlDecode(signatureBase64Url);
|
|
243
|
+
} catch {
|
|
244
|
+
return Buffer.from(signatureBase64Url, "base64");
|
|
245
|
+
}
|
|
246
|
+
})();
|
|
247
|
+
return crypto.verify(null, Buffer.from(payload, "utf8"), key, sig);
|
|
248
|
+
} catch {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
@@ -1,40 +1,40 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* XH Gateway Client
|
|
3
|
-
*
|
|
4
|
-
* A library for device authentication and gateway connection utilities.
|
|
5
|
-
* Provides device identity management, authentication payload building,
|
|
6
|
-
* and device information generation for OpenClaw gateway connections.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
// Types
|
|
10
|
-
export type {
|
|
11
|
-
DeviceIdentity,
|
|
12
|
-
DeviceAuthPayloadParams,
|
|
13
|
-
DeviceAuthPayloadV3Params,
|
|
14
|
-
DeviceInfo,
|
|
15
|
-
DeviceBuilderOptions,
|
|
16
|
-
} from "./types.js";
|
|
17
|
-
|
|
18
|
-
// Device identity management
|
|
19
|
-
export {
|
|
20
|
-
loadOrCreateDeviceIdentity,
|
|
21
|
-
signDevicePayload,
|
|
22
|
-
normalizeDevicePublicKeyBase64Url,
|
|
23
|
-
deriveDeviceIdFromPublicKey,
|
|
24
|
-
publicKeyRawBase64UrlFromPem,
|
|
25
|
-
verifyDeviceSignature,
|
|
26
|
-
} from "./identity.js";
|
|
27
|
-
|
|
28
|
-
// Device authentication
|
|
29
|
-
export {
|
|
30
|
-
buildDeviceAuthPayload,
|
|
31
|
-
buildDeviceAuthPayloadV3,
|
|
32
|
-
normalizeDeviceMetadataForAuth,
|
|
33
|
-
} from "./auth.js";
|
|
34
|
-
|
|
35
|
-
// Device builder
|
|
36
|
-
export {
|
|
37
|
-
buildDeviceInfo,
|
|
38
|
-
buildNodeDeviceInfo,
|
|
39
|
-
buildOperatorDeviceInfo,
|
|
40
|
-
} from "./builder.js";
|
|
1
|
+
/**
|
|
2
|
+
* XH Gateway Client
|
|
3
|
+
*
|
|
4
|
+
* A library for device authentication and gateway connection utilities.
|
|
5
|
+
* Provides device identity management, authentication payload building,
|
|
6
|
+
* and device information generation for OpenClaw gateway connections.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Types
|
|
10
|
+
export type {
|
|
11
|
+
DeviceIdentity,
|
|
12
|
+
DeviceAuthPayloadParams,
|
|
13
|
+
DeviceAuthPayloadV3Params,
|
|
14
|
+
DeviceInfo,
|
|
15
|
+
DeviceBuilderOptions,
|
|
16
|
+
} from "./types.js";
|
|
17
|
+
|
|
18
|
+
// Device identity management
|
|
19
|
+
export {
|
|
20
|
+
loadOrCreateDeviceIdentity,
|
|
21
|
+
signDevicePayload,
|
|
22
|
+
normalizeDevicePublicKeyBase64Url,
|
|
23
|
+
deriveDeviceIdFromPublicKey,
|
|
24
|
+
publicKeyRawBase64UrlFromPem,
|
|
25
|
+
verifyDeviceSignature,
|
|
26
|
+
} from "./identity.js";
|
|
27
|
+
|
|
28
|
+
// Device authentication
|
|
29
|
+
export {
|
|
30
|
+
buildDeviceAuthPayload,
|
|
31
|
+
buildDeviceAuthPayloadV3,
|
|
32
|
+
normalizeDeviceMetadataForAuth,
|
|
33
|
+
} from "./auth.js";
|
|
34
|
+
|
|
35
|
+
// Device builder
|
|
36
|
+
export {
|
|
37
|
+
buildDeviceInfo,
|
|
38
|
+
buildNodeDeviceInfo,
|
|
39
|
+
buildOperatorDeviceInfo,
|
|
40
|
+
} from "./builder.js";
|