@honor-claw/yoyo 0.0.1-alpha.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/index.ts +25 -0
- package/openclaw.plugin.json +28 -0
- package/package.json +59 -0
- package/skills/yoyo-control/SKILL.md +346 -0
- package/skills/yoyo-control/references/capture-screenshot.md +66 -0
- package/skills/yoyo-control/references/local-search.md +27 -0
- package/skills/yoyo-control/references/open-app.md +54 -0
- package/skills/yoyo-control/references/phone-call.md +217 -0
- package/skills/yoyo-control/references/schedule.md +107 -0
- package/skills/yoyo-control/references/screen-recorder.md +67 -0
- package/skills/yoyo-control/references/search-contact.md +37 -0
- package/skills/yoyo-control/references/send-message.md +155 -0
- package/skills/yoyo-control/references/volume.md +536 -0
- package/skills/yoyo-control/scripts/README.md +103 -0
- package/skills/yoyo-control/scripts/invoke.js +119 -0
- package/skills/yoyo-control/scripts/volume-up.json +7 -0
- package/src/apis/claw-cloud.ts +74 -0
- package/src/apis/helpers.ts +10 -0
- package/src/apis/honor-auth.ts +148 -0
- package/src/apis/http-client.ts +239 -0
- package/src/apis/index.ts +8 -0
- package/src/apis/types.ts +47 -0
- package/src/cloud-channel/channel.ts +230 -0
- package/src/cloud-channel/client.ts +312 -0
- package/src/cloud-channel/index.ts +4 -0
- package/src/cloud-channel/types.ts +81 -0
- package/src/commands/index.ts +19 -0
- package/src/commands/login/impl.ts +21 -0
- package/src/commands/login/index.ts +1 -0
- package/src/commands/logout/index.ts +53 -0
- package/src/commands/status/index.ts +64 -0
- package/src/gateway-client/client.deprecated.ts +376 -0
- package/src/gateway-client/client.ts +76 -0
- package/src/gateway-client/device/auth.ts +57 -0
- package/src/gateway-client/device/builder.ts +105 -0
- package/src/gateway-client/device/helpers.ts +40 -0
- package/src/gateway-client/device/identity.ts +251 -0
- package/src/gateway-client/device/index.ts +40 -0
- package/src/gateway-client/device/types.ts +57 -0
- package/src/gateway-client/index.ts +2 -0
- package/src/gateway-client/types.deprecated.ts +217 -0
- package/src/gateway-client/types.ts +8 -0
- package/src/honor-auth/browser.ts +82 -0
- package/src/honor-auth/callback-server.ts +112 -0
- package/src/honor-auth/cloud.ts +70 -0
- package/src/honor-auth/config.ts +35 -0
- package/src/honor-auth/index.ts +2 -0
- package/src/honor-auth/token-manager.ts +80 -0
- package/src/honor-auth/types.ts +43 -0
- package/src/index.ts +10 -0
- package/src/modules/claw-configs/config-manager.ts +172 -0
- package/src/modules/claw-configs/index.ts +7 -0
- package/src/modules/claw-configs/types.ts +30 -0
- package/src/modules/device/device-info.ts +70 -0
- package/src/modules/device/index.ts +3 -0
- package/src/modules/device/providers/base.ts +27 -0
- package/src/modules/device/providers/pad.ts +114 -0
- package/src/modules/device/providers/windows.ts +67 -0
- package/src/modules/device/registry.ts +34 -0
- package/src/modules/login/impl.ts +70 -0
- package/src/modules/login/index.ts +6 -0
- package/src/runtime.ts +14 -0
- package/src/schemas.ts +20 -0
- package/src/services/connection/impl.ts +259 -0
- package/src/services/connection/index.ts +1 -0
- package/src/services/connection/types.ts +20 -0
- package/src/types.ts +64 -0
- package/src/utils/id.ts +8 -0
- package/src/utils/jwt.ts +36 -0
- package/src/utils/logger.ts +20 -0
- package/src/utils/proxy.ts +58 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../types.js";
|
|
2
|
+
import { buildDeviceAuthPayloadV3 } from "./auth.js";
|
|
3
|
+
import {
|
|
4
|
+
publicKeyRawBase64UrlFromPem,
|
|
5
|
+
signDevicePayload,
|
|
6
|
+
} from "./identity.js";
|
|
7
|
+
import {
|
|
8
|
+
type DeviceBuilderOptions,
|
|
9
|
+
type DeviceInfo,
|
|
10
|
+
} from "./types.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Build device information for gateway connection
|
|
14
|
+
* This is the main function to generate device authentication data
|
|
15
|
+
*
|
|
16
|
+
* @param options - Device builder options
|
|
17
|
+
* @returns Device information object or undefined if no device identity provided
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```typescript
|
|
21
|
+
* const deviceInfo = buildDeviceInfo({
|
|
22
|
+
* deviceIdentity: loadOrCreateDeviceIdentity(),
|
|
23
|
+
* clientName: "my-client",
|
|
24
|
+
* role: "node",
|
|
25
|
+
* scopes: ["node.invoke"],
|
|
26
|
+
* nonce: "challenge-nonce",
|
|
27
|
+
* platform: "ios",
|
|
28
|
+
* deviceFamily: "iPhone"
|
|
29
|
+
* });
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export function buildDeviceInfo(options: DeviceBuilderOptions): DeviceInfo | undefined {
|
|
33
|
+
const {
|
|
34
|
+
deviceIdentity,
|
|
35
|
+
clientName = GATEWAY_CLIENT_NAMES.GATEWAY_CLIENT,
|
|
36
|
+
clientMode = GATEWAY_CLIENT_MODES.BACKEND,
|
|
37
|
+
role = "operator",
|
|
38
|
+
scopes = ["operator.admin"],
|
|
39
|
+
platform = process.platform,
|
|
40
|
+
deviceFamily,
|
|
41
|
+
authToken = null,
|
|
42
|
+
nonce,
|
|
43
|
+
signedAtMs = Date.now(),
|
|
44
|
+
} = options;
|
|
45
|
+
|
|
46
|
+
if (!deviceIdentity) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Build authentication payload
|
|
51
|
+
const payload = buildDeviceAuthPayloadV3({
|
|
52
|
+
deviceId: deviceIdentity.deviceId,
|
|
53
|
+
clientId: clientName,
|
|
54
|
+
clientMode,
|
|
55
|
+
role,
|
|
56
|
+
scopes,
|
|
57
|
+
signedAtMs,
|
|
58
|
+
token: authToken,
|
|
59
|
+
nonce,
|
|
60
|
+
platform,
|
|
61
|
+
deviceFamily,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Sign the payload
|
|
65
|
+
const signature = signDevicePayload(deviceIdentity.privateKeyPem, payload);
|
|
66
|
+
|
|
67
|
+
// Return device information
|
|
68
|
+
return {
|
|
69
|
+
id: deviceIdentity.deviceId,
|
|
70
|
+
publicKey: publicKeyRawBase64UrlFromPem(deviceIdentity.publicKeyPem),
|
|
71
|
+
signature,
|
|
72
|
+
signedAt: signedAtMs,
|
|
73
|
+
nonce,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Build device information for node role
|
|
79
|
+
* Convenience function for node connections
|
|
80
|
+
*
|
|
81
|
+
* @param options - Device builder options
|
|
82
|
+
* @returns Device information object or undefined
|
|
83
|
+
*/
|
|
84
|
+
export function buildNodeDeviceInfo(options: Omit<DeviceBuilderOptions, "role">): DeviceInfo | undefined {
|
|
85
|
+
return buildDeviceInfo({
|
|
86
|
+
...options,
|
|
87
|
+
role: "node",
|
|
88
|
+
scopes: options.scopes ?? ["node.invoke"],
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Build device information for operator role
|
|
94
|
+
* Convenience function for operator connections
|
|
95
|
+
*
|
|
96
|
+
* @param options - Device builder options
|
|
97
|
+
* @returns Device information object or undefined
|
|
98
|
+
*/
|
|
99
|
+
export function buildOperatorDeviceInfo(options: Omit<DeviceBuilderOptions, "role">): DeviceInfo | undefined {
|
|
100
|
+
return buildDeviceInfo({
|
|
101
|
+
...options,
|
|
102
|
+
role: "operator",
|
|
103
|
+
scopes: options.scopes ?? ["operator.admin"],
|
|
104
|
+
});
|
|
105
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize device metadata for authentication payloads
|
|
3
|
+
* Keeps cross-runtime normalization deterministic (TS/Swift/Kotlin)
|
|
4
|
+
* by only lowercasing ASCII metadata fields used in auth payloads.
|
|
5
|
+
*/
|
|
6
|
+
function normalizeTrimmedMetadata(value?: string | null): string {
|
|
7
|
+
if (typeof value !== "string") {
|
|
8
|
+
return "";
|
|
9
|
+
}
|
|
10
|
+
const trimmed = value.trim();
|
|
11
|
+
return trimmed ? trimmed : "";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function toLowerAscii(input: string): string {
|
|
15
|
+
return input.replace(/[A-Z]/g, (char) => String.fromCharCode(char.charCodeAt(0) + 32));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function normalizeDeviceMetadataForAuth(value?: string | null): string {
|
|
19
|
+
const trimmed = normalizeTrimmedMetadata(value);
|
|
20
|
+
if (!trimmed) {
|
|
21
|
+
return "";
|
|
22
|
+
}
|
|
23
|
+
// Keep cross-runtime normalization deterministic (TS/Swift/Kotlin) by only
|
|
24
|
+
// lowercasing ASCII metadata fields used in auth payloads.
|
|
25
|
+
return toLowerAscii(trimmed);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Normalize device metadata for policy classification
|
|
30
|
+
* Collapses Unicode confusables to stable ASCII-like tokens before matching platform/family rules.
|
|
31
|
+
*/
|
|
32
|
+
export function normalizeDeviceMetadataForPolicy(value?: string | null): string {
|
|
33
|
+
const trimmed = normalizeTrimmedMetadata(value);
|
|
34
|
+
if (!trimmed) {
|
|
35
|
+
return "";
|
|
36
|
+
}
|
|
37
|
+
// Policy classification should collapse Unicode confusables to stable ASCII-ish
|
|
38
|
+
// tokens where possible before matching platform/family rules.
|
|
39
|
+
return trimmed.normalize("NFKD").replace(/\p{M}/gu, "").toLowerCase();
|
|
40
|
+
}
|
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +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";
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device identity information
|
|
3
|
+
*/
|
|
4
|
+
export type DeviceIdentity = {
|
|
5
|
+
deviceId: string;
|
|
6
|
+
publicKeyPem: string;
|
|
7
|
+
privateKeyPem: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Device authentication payload parameters
|
|
12
|
+
*/
|
|
13
|
+
export type DeviceAuthPayloadParams = {
|
|
14
|
+
deviceId: string;
|
|
15
|
+
clientId: string;
|
|
16
|
+
clientMode: string;
|
|
17
|
+
role: string;
|
|
18
|
+
scopes: string[];
|
|
19
|
+
signedAtMs: number;
|
|
20
|
+
token?: string | null;
|
|
21
|
+
nonce: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Device authentication payload V3 parameters (with platform/device family)
|
|
26
|
+
*/
|
|
27
|
+
export type DeviceAuthPayloadV3Params = DeviceAuthPayloadParams & {
|
|
28
|
+
platform?: string | null;
|
|
29
|
+
deviceFamily?: string | null;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Device information for gateway connection
|
|
34
|
+
*/
|
|
35
|
+
export type DeviceInfo = {
|
|
36
|
+
id: string;
|
|
37
|
+
publicKey: string;
|
|
38
|
+
signature: string;
|
|
39
|
+
signedAt: number;
|
|
40
|
+
nonce: string;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Device builder options
|
|
45
|
+
*/
|
|
46
|
+
export type DeviceBuilderOptions = {
|
|
47
|
+
deviceIdentity?: DeviceIdentity;
|
|
48
|
+
clientName?: string;
|
|
49
|
+
clientMode?: string;
|
|
50
|
+
role?: string;
|
|
51
|
+
scopes?: string[];
|
|
52
|
+
platform?: string;
|
|
53
|
+
deviceFamily?: string;
|
|
54
|
+
authToken?: string | null;
|
|
55
|
+
nonce: string;
|
|
56
|
+
signedAtMs?: number;
|
|
57
|
+
};
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// ==================== 类型定义 ====================
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Gateway Client ID 枚举
|
|
5
|
+
*/
|
|
6
|
+
export const GATEWAY_CLIENT_IDS = {
|
|
7
|
+
WEBCHAT_UI: "webchat-ui",
|
|
8
|
+
CONTROL_UI: "openclaw-control-ui",
|
|
9
|
+
WEBCHAT: "webchat",
|
|
10
|
+
CLI: "cli",
|
|
11
|
+
GATEWAY_CLIENT: "gateway-client",
|
|
12
|
+
MACOS_APP: "openclaw-macos",
|
|
13
|
+
IOS_APP: "openclaw-ios",
|
|
14
|
+
ANDROID_APP: "openclaw-android",
|
|
15
|
+
NODE_HOST: "node-host",
|
|
16
|
+
TEST: "test",
|
|
17
|
+
FINGERPRINT: "fingerprint",
|
|
18
|
+
PROBE: "openclaw-probe",
|
|
19
|
+
} as const;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Gateway Client Mode 枚举
|
|
23
|
+
*/
|
|
24
|
+
export const GATEWAY_CLIENT_MODES = {
|
|
25
|
+
WEBCHAT: "webchat",
|
|
26
|
+
CLI: "cli",
|
|
27
|
+
UI: "ui",
|
|
28
|
+
BACKEND: "backend",
|
|
29
|
+
NODE: "node",
|
|
30
|
+
PROBE: "probe",
|
|
31
|
+
TEST: "test",
|
|
32
|
+
} as const;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Gateway Client ID 类型
|
|
36
|
+
*/
|
|
37
|
+
export type GatewayClientId =
|
|
38
|
+
(typeof GATEWAY_CLIENT_IDS)[keyof typeof GATEWAY_CLIENT_IDS];
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Gateway Client Mode 类型
|
|
42
|
+
*/
|
|
43
|
+
export type GatewayClientMode =
|
|
44
|
+
(typeof GATEWAY_CLIENT_MODES)[keyof typeof GATEWAY_CLIENT_MODES];
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Back-compat naming (internal): these values are IDs, not display names.
|
|
48
|
+
*/
|
|
49
|
+
export const GATEWAY_CLIENT_NAMES = GATEWAY_CLIENT_IDS;
|
|
50
|
+
export type GatewayClientName = GatewayClientId;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Gateway Client Info 类型
|
|
54
|
+
*/
|
|
55
|
+
export type GatewayClientInfo = {
|
|
56
|
+
id: GatewayClientId;
|
|
57
|
+
displayName?: string;
|
|
58
|
+
version: string;
|
|
59
|
+
platform: string;
|
|
60
|
+
deviceFamily?: string;
|
|
61
|
+
modelIdentifier?: string;
|
|
62
|
+
mode: GatewayClientMode;
|
|
63
|
+
instanceId?: string;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Gateway Client Cap 枚举
|
|
68
|
+
*/
|
|
69
|
+
export const GATEWAY_CLIENT_CAPS = {
|
|
70
|
+
TOOL_EVENTS: "tool-events",
|
|
71
|
+
} as const;
|
|
72
|
+
|
|
73
|
+
export type GatewayClientCap =
|
|
74
|
+
(typeof GATEWAY_CLIENT_CAPS)[keyof typeof GATEWAY_CLIENT_CAPS];
|
|
75
|
+
|
|
76
|
+
const GATEWAY_CLIENT_ID_SET = new Set<GatewayClientId>(
|
|
77
|
+
Object.values(GATEWAY_CLIENT_IDS)
|
|
78
|
+
);
|
|
79
|
+
const GATEWAY_CLIENT_MODE_SET = new Set<GatewayClientMode>(
|
|
80
|
+
Object.values(GATEWAY_CLIENT_MODES)
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
export function normalizeGatewayClientId(
|
|
84
|
+
raw?: string | null
|
|
85
|
+
): GatewayClientId | undefined {
|
|
86
|
+
const normalized = raw?.trim().toLowerCase();
|
|
87
|
+
if (!normalized) {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
return GATEWAY_CLIENT_ID_SET.has(normalized as GatewayClientId)
|
|
91
|
+
? (normalized as GatewayClientId)
|
|
92
|
+
: undefined;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function normalizeGatewayClientName(
|
|
96
|
+
raw?: string | null
|
|
97
|
+
): GatewayClientName | undefined {
|
|
98
|
+
return normalizeGatewayClientId(raw);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function normalizeGatewayClientMode(
|
|
102
|
+
raw?: string | null
|
|
103
|
+
): GatewayClientMode | undefined {
|
|
104
|
+
const normalized = raw?.trim().toLowerCase();
|
|
105
|
+
if (!normalized) {
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
return GATEWAY_CLIENT_MODE_SET.has(normalized as GatewayClientMode)
|
|
109
|
+
? (normalized as GatewayClientMode)
|
|
110
|
+
: undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function hasGatewayClientCap(
|
|
114
|
+
caps: string[] | null | undefined,
|
|
115
|
+
cap: GatewayClientCap
|
|
116
|
+
): boolean {
|
|
117
|
+
if (!Array.isArray(caps)) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
return caps.includes(cap);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 协议帧类型 - 请求帧
|
|
125
|
+
*/
|
|
126
|
+
export interface RequestFrame {
|
|
127
|
+
type: "req";
|
|
128
|
+
id: string;
|
|
129
|
+
method: string;
|
|
130
|
+
params?: unknown;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* 协议帧类型 - 响应帧
|
|
135
|
+
*/
|
|
136
|
+
export interface ResponseFrame {
|
|
137
|
+
type: "res";
|
|
138
|
+
id: string;
|
|
139
|
+
ok: boolean;
|
|
140
|
+
payload?: unknown;
|
|
141
|
+
error?: {
|
|
142
|
+
code: number;
|
|
143
|
+
message: string;
|
|
144
|
+
details?: unknown;
|
|
145
|
+
retryable?: boolean;
|
|
146
|
+
retryAfterMs?: boolean;
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 协议帧类型 - 事件帧
|
|
152
|
+
*/
|
|
153
|
+
export interface EventFrame {
|
|
154
|
+
type: "evt" | "event";
|
|
155
|
+
event: string;
|
|
156
|
+
seq?: number;
|
|
157
|
+
payload?: unknown;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* 连接成功响应
|
|
162
|
+
*/
|
|
163
|
+
export interface HelloOk {
|
|
164
|
+
type: "hello-ok";
|
|
165
|
+
protocol: number;
|
|
166
|
+
gateway: {
|
|
167
|
+
id: string;
|
|
168
|
+
version: string;
|
|
169
|
+
};
|
|
170
|
+
auth?: {
|
|
171
|
+
deviceToken?: string;
|
|
172
|
+
role?: string;
|
|
173
|
+
scopes?: string[];
|
|
174
|
+
};
|
|
175
|
+
policy?: {
|
|
176
|
+
tickIntervalMs?: number;
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* 连接参数(简化版)
|
|
182
|
+
*/
|
|
183
|
+
export interface ConnectParams {
|
|
184
|
+
minProtocol?: number;
|
|
185
|
+
maxProtocol?: number;
|
|
186
|
+
client: {
|
|
187
|
+
id: GatewayClientId;
|
|
188
|
+
displayName?: string;
|
|
189
|
+
version?: string;
|
|
190
|
+
platform?: string;
|
|
191
|
+
deviceFamily?: string;
|
|
192
|
+
modelIdentifier?: string;
|
|
193
|
+
mode: GatewayClientMode;
|
|
194
|
+
instanceId?: string;
|
|
195
|
+
};
|
|
196
|
+
caps?: string[];
|
|
197
|
+
commands?: string[];
|
|
198
|
+
permissions?: Record<string, boolean>;
|
|
199
|
+
pathEnv?: string;
|
|
200
|
+
role?: string;
|
|
201
|
+
scopes?: string[];
|
|
202
|
+
device?: {
|
|
203
|
+
id: string;
|
|
204
|
+
publicKey: string;
|
|
205
|
+
signature: string;
|
|
206
|
+
signedAt: number;
|
|
207
|
+
nonce: string;
|
|
208
|
+
};
|
|
209
|
+
auth?: {
|
|
210
|
+
token?: string;
|
|
211
|
+
deviceToken?: string;
|
|
212
|
+
password?: string;
|
|
213
|
+
};
|
|
214
|
+
modelIdentifier?: string;
|
|
215
|
+
locale?: string;
|
|
216
|
+
userAgent?: string;
|
|
217
|
+
}
|