phoneclaw-connector 1.0.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 +228 -0
- package/SKILL.md +203 -0
- package/dist/device-auth-store.d.ts +29 -0
- package/dist/device-auth-store.d.ts.map +1 -0
- package/dist/device-auth-store.js +75 -0
- package/dist/device-auth-store.js.map +1 -0
- package/dist/device-identity.d.ts +41 -0
- package/dist/device-identity.d.ts.map +1 -0
- package/dist/device-identity.js +133 -0
- package/dist/device-identity.js.map +1 -0
- package/dist/device.d.ts +21 -0
- package/dist/device.d.ts.map +1 -0
- package/dist/device.js +104 -0
- package/dist/device.js.map +1 -0
- package/dist/gateway.d.ts +58 -0
- package/dist/gateway.d.ts.map +1 -0
- package/dist/gateway.js +314 -0
- package/dist/gateway.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +434 -0
- package/dist/index.js.map +1 -0
- package/dist/pair.d.ts +2 -0
- package/dist/pair.d.ts.map +1 -0
- package/dist/pair.js +98 -0
- package/dist/pair.js.map +1 -0
- package/dist/pairing.d.ts +12 -0
- package/dist/pairing.d.ts.map +1 -0
- package/dist/pairing.js +71 -0
- package/dist/pairing.js.map +1 -0
- package/dist/relay.d.ts +25 -0
- package/dist/relay.d.ts.map +1 -0
- package/dist/relay.js +206 -0
- package/dist/relay.js.map +1 -0
- package/dist/status.d.ts +2 -0
- package/dist/status.d.ts.map +1 -0
- package/dist/status.js +34 -0
- package/dist/status.js.map +1 -0
- package/dist/types.d.ts +33 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +53 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { createHash, createPrivateKey, createPublicKey, generateKeyPairSync, sign as cryptoSign, } from 'crypto';
|
|
2
|
+
import { promises as fs } from 'fs';
|
|
3
|
+
import { join, dirname } from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = dirname(__filename);
|
|
7
|
+
const IDENTITY_FILE = 'device-identity.json';
|
|
8
|
+
function base64UrlEncode(buf) {
|
|
9
|
+
return buf
|
|
10
|
+
.toString('base64')
|
|
11
|
+
.replaceAll('+', '-')
|
|
12
|
+
.replaceAll('/', '_')
|
|
13
|
+
.replace(/=+$/g, '');
|
|
14
|
+
}
|
|
15
|
+
function deriveDeviceIdFromPublicKeyPem(publicKeyPem) {
|
|
16
|
+
const publicKey = createPublicKey(publicKeyPem);
|
|
17
|
+
const spki = publicKey.export({ type: 'spki', format: 'der' });
|
|
18
|
+
// Ed25519 SPKI: 12-byte prefix + 32-byte raw key
|
|
19
|
+
const ED25519_SPKI_PREFIX = Buffer.from('302a300506032b6570032100', 'hex');
|
|
20
|
+
let raw;
|
|
21
|
+
if (spki.length === ED25519_SPKI_PREFIX.length + 32 &&
|
|
22
|
+
spki.subarray(0, ED25519_SPKI_PREFIX.length).equals(ED25519_SPKI_PREFIX)) {
|
|
23
|
+
raw = spki.subarray(ED25519_SPKI_PREFIX.length);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
raw = spki;
|
|
27
|
+
}
|
|
28
|
+
// OpenClaw uses SHA-256 hex (64 lowercase hex chars) as the stable deviceId.
|
|
29
|
+
return createHash('sha256').update(raw).digest('hex');
|
|
30
|
+
}
|
|
31
|
+
function publicKeyRawBase64UrlFromPem(publicKeyPem) {
|
|
32
|
+
const publicKey = createPublicKey(publicKeyPem);
|
|
33
|
+
const spki = publicKey.export({ type: 'spki', format: 'der' });
|
|
34
|
+
const ED25519_SPKI_PREFIX = Buffer.from('302a300506032b6570032100', 'hex');
|
|
35
|
+
let raw;
|
|
36
|
+
if (spki.length === ED25519_SPKI_PREFIX.length + 32 &&
|
|
37
|
+
spki.subarray(0, ED25519_SPKI_PREFIX.length).equals(ED25519_SPKI_PREFIX)) {
|
|
38
|
+
raw = spki.subarray(ED25519_SPKI_PREFIX.length);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
raw = spki;
|
|
42
|
+
}
|
|
43
|
+
return base64UrlEncode(raw);
|
|
44
|
+
}
|
|
45
|
+
function generateIdentity() {
|
|
46
|
+
const { publicKey, privateKey } = generateKeyPairSync('ed25519');
|
|
47
|
+
const publicKeyPem = publicKey.export({ type: 'spki', format: 'pem' });
|
|
48
|
+
const privateKeyPem = privateKey.export({ type: 'pkcs8', format: 'pem' });
|
|
49
|
+
const deviceId = deriveDeviceIdFromPublicKeyPem(publicKeyPem);
|
|
50
|
+
return {
|
|
51
|
+
deviceId,
|
|
52
|
+
publicKey: publicKeyRawBase64UrlFromPem(publicKeyPem),
|
|
53
|
+
privateKey: privateKeyPem,
|
|
54
|
+
publicKeyPem,
|
|
55
|
+
createdAt: new Date().toISOString(),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function isValidIdentity(value) {
|
|
59
|
+
if (!value || typeof value !== 'object')
|
|
60
|
+
return false;
|
|
61
|
+
const v = value;
|
|
62
|
+
return (typeof v.deviceId === 'string' &&
|
|
63
|
+
typeof v.publicKey === 'string' &&
|
|
64
|
+
typeof v.privateKey === 'string' &&
|
|
65
|
+
typeof v.publicKeyPem === 'string' &&
|
|
66
|
+
typeof v.createdAt === 'string');
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Manages the skill's persistent Ed25519 device identity used for OpenClaw
|
|
70
|
+
* Gateway pairing. The identity is stored next to the skill source so that
|
|
71
|
+
* re-launches keep the same deviceId and can be auto-approved as a paired
|
|
72
|
+
* node.
|
|
73
|
+
*/
|
|
74
|
+
export class DeviceIdentityStore {
|
|
75
|
+
identityPath;
|
|
76
|
+
identity = null;
|
|
77
|
+
constructor(configDir) {
|
|
78
|
+
this.identityPath = configDir
|
|
79
|
+
? join(configDir, IDENTITY_FILE)
|
|
80
|
+
: join(__dirname, '..', IDENTITY_FILE);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Load existing identity or generate a new one. Persists to disk on first
|
|
84
|
+
* creation. Subsequent calls return the in-memory cached identity.
|
|
85
|
+
*/
|
|
86
|
+
async getOrCreate() {
|
|
87
|
+
if (this.identity)
|
|
88
|
+
return this.identity;
|
|
89
|
+
try {
|
|
90
|
+
const data = await fs.readFile(this.identityPath, 'utf-8');
|
|
91
|
+
const parsed = JSON.parse(data);
|
|
92
|
+
if (isValidIdentity(parsed) && this.matchesPublicKey(parsed)) {
|
|
93
|
+
this.identity = parsed;
|
|
94
|
+
return parsed;
|
|
95
|
+
}
|
|
96
|
+
console.warn('[DeviceIdentity] Existing identity file is invalid or mismatched; regenerating.');
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// file does not exist or is unreadable — generate new
|
|
100
|
+
}
|
|
101
|
+
const generated = generateIdentity();
|
|
102
|
+
await this.persist(generated);
|
|
103
|
+
this.identity = generated;
|
|
104
|
+
return generated;
|
|
105
|
+
}
|
|
106
|
+
/** Verify the persisted publicKey (base64url raw) actually matches the PEM. */
|
|
107
|
+
matchesPublicKey(identity) {
|
|
108
|
+
try {
|
|
109
|
+
const derivedRaw = publicKeyRawBase64UrlFromPem(identity.publicKeyPem);
|
|
110
|
+
return derivedRaw === identity.publicKey;
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async persist(identity) {
|
|
117
|
+
await fs.mkdir(dirname(this.identityPath), { recursive: true });
|
|
118
|
+
await fs.writeFile(this.identityPath, JSON.stringify(identity, null, 2), {
|
|
119
|
+
mode: 0o600,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Sign a payload string with the identity's Ed25519 private key. Returns
|
|
124
|
+
* base64url-encoded raw signature (no padding).
|
|
125
|
+
*/
|
|
126
|
+
signPayload(payload, identity) {
|
|
127
|
+
const privateKey = createPrivateKey(identity.privateKey);
|
|
128
|
+
const signature = cryptoSign(null, Buffer.from(payload, 'utf8'), privateKey);
|
|
129
|
+
return base64UrlEncode(signature);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
export { base64UrlEncode, publicKeyRawBase64UrlFromPem, deriveDeviceIdFromPublicKeyPem };
|
|
133
|
+
//# sourceMappingURL=device-identity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"device-identity.js","sourceRoot":"","sources":["../src/device-identity.ts"],"names":[],"mappings":"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"}
|
package/dist/device.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { DeviceInfo } from './types.js';
|
|
2
|
+
import { DeviceIdentity } from './device-identity.js';
|
|
3
|
+
export declare class DeviceManager {
|
|
4
|
+
private configPath;
|
|
5
|
+
private configDir;
|
|
6
|
+
private deviceInfo;
|
|
7
|
+
private identityStore;
|
|
8
|
+
private cachedIdentity;
|
|
9
|
+
constructor(configDir?: string);
|
|
10
|
+
getDeviceInfo(): Promise<DeviceInfo>;
|
|
11
|
+
/**
|
|
12
|
+
* Returns the persistent Ed25519 device identity used for OpenClaw Gateway
|
|
13
|
+
* pairing. Lazily creates one on first call and caches it in memory.
|
|
14
|
+
*/
|
|
15
|
+
getDeviceIdentity(): Promise<DeviceIdentity>;
|
|
16
|
+
private generateDeviceInfo;
|
|
17
|
+
private getMachineId;
|
|
18
|
+
private saveDeviceInfo;
|
|
19
|
+
resetDevice(): Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=device.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"device.d.ts","sourceRoot":"","sources":["../src/device.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAOxC,OAAO,EAAE,cAAc,EAAuB,MAAM,sBAAsB,CAAC;AAM3E,qBAAa,aAAa;IACxB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,UAAU,CAA2B;IAC7C,OAAO,CAAC,aAAa,CAAsB;IAC3C,OAAO,CAAC,cAAc,CAA+B;gBAEzC,SAAS,CAAC,EAAE,MAAM;IAMxB,aAAa,IAAI,OAAO,CAAC,UAAU,CAAC;IAoB1C;;;OAGG;IACG,iBAAiB,IAAI,OAAO,CAAC,cAAc,CAAC;YAMpC,kBAAkB;YAsBlB,YAAY;YAmBZ,cAAc;IAQtB,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;CASnC"}
|
package/dist/device.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { createHash } from 'crypto';
|
|
2
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
3
|
+
import { promises as fs } from 'fs';
|
|
4
|
+
import { join, dirname } from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import { exec } from 'child_process';
|
|
7
|
+
import { promisify } from 'util';
|
|
8
|
+
import os from 'os';
|
|
9
|
+
import { DeviceIdentityStore } from './device-identity.js';
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = dirname(__filename);
|
|
12
|
+
const execAsync = promisify(exec);
|
|
13
|
+
export class DeviceManager {
|
|
14
|
+
configPath;
|
|
15
|
+
configDir;
|
|
16
|
+
deviceInfo = null;
|
|
17
|
+
identityStore;
|
|
18
|
+
cachedIdentity = null;
|
|
19
|
+
constructor(configDir) {
|
|
20
|
+
this.configDir = configDir || join(__dirname, '..');
|
|
21
|
+
this.configPath = join(this.configDir, 'device.json');
|
|
22
|
+
this.identityStore = new DeviceIdentityStore(this.configDir);
|
|
23
|
+
}
|
|
24
|
+
async getDeviceInfo() {
|
|
25
|
+
if (this.deviceInfo) {
|
|
26
|
+
return this.deviceInfo;
|
|
27
|
+
}
|
|
28
|
+
// Try to load existing device info
|
|
29
|
+
try {
|
|
30
|
+
const data = await fs.readFile(this.configPath, 'utf-8');
|
|
31
|
+
const parsed = JSON.parse(data);
|
|
32
|
+
this.deviceInfo = parsed;
|
|
33
|
+
return parsed;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// Generate new device info
|
|
37
|
+
const generated = await this.generateDeviceInfo();
|
|
38
|
+
this.deviceInfo = generated;
|
|
39
|
+
await this.saveDeviceInfo();
|
|
40
|
+
return generated;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Returns the persistent Ed25519 device identity used for OpenClaw Gateway
|
|
45
|
+
* pairing. Lazily creates one on first call and caches it in memory.
|
|
46
|
+
*/
|
|
47
|
+
async getDeviceIdentity() {
|
|
48
|
+
if (this.cachedIdentity)
|
|
49
|
+
return this.cachedIdentity;
|
|
50
|
+
this.cachedIdentity = await this.identityStore.getOrCreate();
|
|
51
|
+
return this.cachedIdentity;
|
|
52
|
+
}
|
|
53
|
+
async generateDeviceInfo() {
|
|
54
|
+
// Generate device ID based on machine fingerprint
|
|
55
|
+
const machineId = await this.getMachineId();
|
|
56
|
+
const hostname = os.hostname();
|
|
57
|
+
// 使用 "主机名-机器指纹前8位" 作为 deviceId,既唯一又易读
|
|
58
|
+
const fingerprint = createHash('sha256')
|
|
59
|
+
.update(`phoneclaw-${machineId}`)
|
|
60
|
+
.digest('hex')
|
|
61
|
+
.slice(0, 8);
|
|
62
|
+
const deviceId = `${hostname}-${fingerprint}`;
|
|
63
|
+
// Generate device key for authentication
|
|
64
|
+
const deviceKey = uuidv4();
|
|
65
|
+
return {
|
|
66
|
+
deviceId,
|
|
67
|
+
deviceKey,
|
|
68
|
+
platform: process.platform,
|
|
69
|
+
version: '1.0.0'
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
async getMachineId() {
|
|
73
|
+
try {
|
|
74
|
+
if (process.platform === 'darwin') {
|
|
75
|
+
const { stdout } = await execAsync('ioreg -rd1 -c IOPlatformExpertDevice');
|
|
76
|
+
const match = stdout.match(/"IOPlatformUUID" = "([^"]+)"/);
|
|
77
|
+
if (match)
|
|
78
|
+
return match[1];
|
|
79
|
+
}
|
|
80
|
+
if (process.platform === 'linux') {
|
|
81
|
+
const machineId = await fs.readFile('/etc/machine-id', 'utf-8');
|
|
82
|
+
return machineId.trim();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// Fallback to hostname
|
|
87
|
+
}
|
|
88
|
+
return os.hostname();
|
|
89
|
+
}
|
|
90
|
+
async saveDeviceInfo() {
|
|
91
|
+
await fs.writeFile(this.configPath, JSON.stringify(this.deviceInfo, null, 2), 'utf-8');
|
|
92
|
+
}
|
|
93
|
+
async resetDevice() {
|
|
94
|
+
this.deviceInfo = null;
|
|
95
|
+
this.cachedIdentity = null;
|
|
96
|
+
try {
|
|
97
|
+
await fs.unlink(this.configPath);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// File doesn't exist, ignore
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=device.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"device.js","sourceRoot":"","sources":["../src/device.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { SkillConfig } from './types.js';
|
|
2
|
+
import { DeviceIdentity } from './device-identity.js';
|
|
3
|
+
import { DeviceAuthStore } from './device-auth-store.js';
|
|
4
|
+
export interface GatewayClientOptions {
|
|
5
|
+
config: SkillConfig;
|
|
6
|
+
deviceIdentity: DeviceIdentity;
|
|
7
|
+
deviceAuthStore: DeviceAuthStore;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* OpenClaw Gateway WebSocket client. Performs the device identity pairing
|
|
11
|
+
* flow (connect.challenge -> signed connect -> hello-ok with persisted
|
|
12
|
+
* deviceToken) so that operator.write / operator.read scopes are granted.
|
|
13
|
+
*/
|
|
14
|
+
export declare class GatewayClient {
|
|
15
|
+
private ws;
|
|
16
|
+
private config;
|
|
17
|
+
private deviceIdentity;
|
|
18
|
+
private deviceAuthStore;
|
|
19
|
+
private isConnected;
|
|
20
|
+
private requestHandlers;
|
|
21
|
+
private requestIdCounter;
|
|
22
|
+
private eventHandlers;
|
|
23
|
+
constructor(options: GatewayClientOptions);
|
|
24
|
+
connect(): Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* 1. Load any previously-persisted deviceToken for this identity.
|
|
27
|
+
* 2. Wait for `connect.challenge` from gateway (gives us a nonce).
|
|
28
|
+
* 3. Build a v3 device-auth payload, sign with our Ed25519 private key.
|
|
29
|
+
* 4. Send `connect` request with the signed device block + token/deviceToken.
|
|
30
|
+
* 5. On hello-ok, persist any newly issued deviceToken.
|
|
31
|
+
*/
|
|
32
|
+
private runHandshake;
|
|
33
|
+
/** Wait for the gateway to emit a connect.challenge event with a nonce. */
|
|
34
|
+
private waitForConnectChallenge;
|
|
35
|
+
/**
|
|
36
|
+
* Build the v3 device-auth payload and sign it with the device's Ed25519
|
|
37
|
+
* private key. Mirrors openclaw's `buildDeviceAuthPayloadV3` exactly so
|
|
38
|
+
* the gateway can verify the signature.
|
|
39
|
+
*/
|
|
40
|
+
private buildDeviceConnectParams;
|
|
41
|
+
private signDevicePayload;
|
|
42
|
+
/** Persist newly issued deviceToken from hello-ok response. */
|
|
43
|
+
private persistHelloOk;
|
|
44
|
+
private handleMessage;
|
|
45
|
+
private handleEvent;
|
|
46
|
+
/** Subscribe to gateway events. Pass '*' to receive all events. */
|
|
47
|
+
onEvent(eventName: string, handler: (event: any) => void): void;
|
|
48
|
+
offEvent(eventName: string): void;
|
|
49
|
+
private sendRequest;
|
|
50
|
+
agent(message: string, sessionId?: string): Promise<any>;
|
|
51
|
+
health(): Promise<any>;
|
|
52
|
+
status(): Promise<any>;
|
|
53
|
+
nodeList(): Promise<any>;
|
|
54
|
+
send(message: string): Promise<any>;
|
|
55
|
+
get connected(): boolean;
|
|
56
|
+
disconnect(): void;
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=gateway.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gateway.d.ts","sourceRoot":"","sources":["../src/gateway.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EACL,cAAc,EAGf,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,eAAe,EAAqB,MAAM,wBAAwB,CAAC;AAwD5E,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,WAAW,CAAC;IACpB,cAAc,EAAE,cAAc,CAAC;IAC/B,eAAe,EAAE,eAAe,CAAC;CAClC;AAED;;;;GAIG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,EAAE,CAA0B;IACpC,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,cAAc,CAAiB;IACvC,OAAO,CAAC,eAAe,CAAkB;IACzC,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,eAAe,CAA8D;IACrF,OAAO,CAAC,gBAAgB,CAAK;IAC7B,OAAO,CAAC,aAAa,CAA2C;gBAEpD,OAAO,EAAE,oBAAoB;IAMnC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAoD9B;;;;;;OAMG;YACW,YAAY;IAgD1B,2EAA2E;IAC3E,OAAO,CAAC,uBAAuB;IA+B/B;;;;OAIG;IACH,OAAO,CAAC,wBAAwB;IAiChC,OAAO,CAAC,iBAAiB;IAOzB,+DAA+D;YACjD,cAAc;IAqB5B,OAAO,CAAC,aAAa;IAyBrB,OAAO,CAAC,WAAW;IAqBnB,mEAAmE;IACnE,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,IAAI,GAAG,IAAI;IAI/D,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAIjC,OAAO,CAAC,WAAW;IAuBb,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC;IAkBxD,MAAM,IAAI,OAAO,CAAC,GAAG,CAAC;IAItB,MAAM,IAAI,OAAO,CAAC,GAAG,CAAC;IAItB,QAAQ,IAAI,OAAO,CAAC,GAAG,CAAC;IAIxB,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC;IAIzC,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,UAAU,IAAI,IAAI;CAOnB"}
|
package/dist/gateway.js
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import WebSocket from 'ws';
|
|
2
|
+
import { randomUUID } from 'crypto';
|
|
3
|
+
import { DeviceIdentityStore, publicKeyRawBase64UrlFromPem, } from './device-identity.js';
|
|
4
|
+
const ROLE = 'operator';
|
|
5
|
+
const SCOPES = ['operator.read', 'operator.write'];
|
|
6
|
+
const CAPS = ['agent', 'tools', 'sessions'];
|
|
7
|
+
const CLIENT_ID = 'node-host';
|
|
8
|
+
const CLIENT_DISPLAY = 'PhoneClaw Skill';
|
|
9
|
+
const CLIENT_VERSION = '1.0.0';
|
|
10
|
+
const PROTOCOL = 4;
|
|
11
|
+
const CONNECT_CHALLENGE_TIMEOUT_MS = 10_000;
|
|
12
|
+
/**
|
|
13
|
+
* OpenClaw Gateway WebSocket client. Performs the device identity pairing
|
|
14
|
+
* flow (connect.challenge -> signed connect -> hello-ok with persisted
|
|
15
|
+
* deviceToken) so that operator.write / operator.read scopes are granted.
|
|
16
|
+
*/
|
|
17
|
+
export class GatewayClient {
|
|
18
|
+
ws = null;
|
|
19
|
+
config;
|
|
20
|
+
deviceIdentity;
|
|
21
|
+
deviceAuthStore;
|
|
22
|
+
isConnected = false;
|
|
23
|
+
requestHandlers = new Map();
|
|
24
|
+
requestIdCounter = 0;
|
|
25
|
+
eventHandlers = new Map();
|
|
26
|
+
constructor(options) {
|
|
27
|
+
this.config = options.config;
|
|
28
|
+
this.deviceIdentity = options.deviceIdentity;
|
|
29
|
+
this.deviceAuthStore = options.deviceAuthStore;
|
|
30
|
+
}
|
|
31
|
+
async connect() {
|
|
32
|
+
return new Promise((resolve, reject) => {
|
|
33
|
+
try {
|
|
34
|
+
const url = this.config.gatewayToken
|
|
35
|
+
? `${this.config.gatewayUrl}?token=${this.config.gatewayToken}`
|
|
36
|
+
: this.config.gatewayUrl;
|
|
37
|
+
this.ws = new WebSocket(url);
|
|
38
|
+
this.ws.on('open', () => {
|
|
39
|
+
console.log('[GatewayClient] Connected to OpenClaw Gateway');
|
|
40
|
+
this.isConnected = true;
|
|
41
|
+
});
|
|
42
|
+
this.ws.on('message', (data) => {
|
|
43
|
+
try {
|
|
44
|
+
const message = JSON.parse(data.toString());
|
|
45
|
+
this.handleMessage(message);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
console.error('[GatewayClient] Failed to parse message:', error);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
this.ws.on('close', () => {
|
|
52
|
+
console.log('[GatewayClient] Gateway connection closed');
|
|
53
|
+
this.isConnected = false;
|
|
54
|
+
});
|
|
55
|
+
this.ws.on('error', (error) => {
|
|
56
|
+
console.error('[GatewayClient] Gateway connection error:', error);
|
|
57
|
+
if (this.ws) {
|
|
58
|
+
try {
|
|
59
|
+
this.ws.removeAllListeners();
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// Ignore
|
|
63
|
+
}
|
|
64
|
+
this.ws = null;
|
|
65
|
+
}
|
|
66
|
+
reject(error);
|
|
67
|
+
});
|
|
68
|
+
// Kick off the handshake asynchronously; the 'open' resolve is
|
|
69
|
+
// intentionally omitted because handshake is what defines success.
|
|
70
|
+
this.runHandshake()
|
|
71
|
+
.then(() => resolve())
|
|
72
|
+
.catch((err) => reject(err));
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
reject(error);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* 1. Load any previously-persisted deviceToken for this identity.
|
|
81
|
+
* 2. Wait for `connect.challenge` from gateway (gives us a nonce).
|
|
82
|
+
* 3. Build a v3 device-auth payload, sign with our Ed25519 private key.
|
|
83
|
+
* 4. Send `connect` request with the signed device block + token/deviceToken.
|
|
84
|
+
* 5. On hello-ok, persist any newly issued deviceToken.
|
|
85
|
+
*/
|
|
86
|
+
async runHandshake() {
|
|
87
|
+
const stored = await this.deviceAuthStore.loadToken(ROLE, this.deviceIdentity.deviceId);
|
|
88
|
+
const challenge = await this.waitForConnectChallenge();
|
|
89
|
+
const signedAt = Date.now();
|
|
90
|
+
const device = this.buildDeviceConnectParams({
|
|
91
|
+
nonce: challenge.nonce,
|
|
92
|
+
signedAt,
|
|
93
|
+
});
|
|
94
|
+
const params = {
|
|
95
|
+
minProtocol: PROTOCOL,
|
|
96
|
+
maxProtocol: PROTOCOL,
|
|
97
|
+
client: {
|
|
98
|
+
id: CLIENT_ID,
|
|
99
|
+
displayName: CLIENT_DISPLAY,
|
|
100
|
+
version: CLIENT_VERSION,
|
|
101
|
+
platform: process.platform,
|
|
102
|
+
mode: 'node',
|
|
103
|
+
},
|
|
104
|
+
role: ROLE,
|
|
105
|
+
scopes: SCOPES,
|
|
106
|
+
caps: CAPS,
|
|
107
|
+
auth: {
|
|
108
|
+
token: this.config.gatewayToken || undefined,
|
|
109
|
+
deviceToken: stored?.token,
|
|
110
|
+
},
|
|
111
|
+
device,
|
|
112
|
+
};
|
|
113
|
+
const response = await this.sendRequest('connect', params);
|
|
114
|
+
if (response?.type !== 'hello-ok') {
|
|
115
|
+
throw new Error(`Gateway handshake failed: unexpected payload ${JSON.stringify(response)}`);
|
|
116
|
+
}
|
|
117
|
+
await this.persistHelloOk(response);
|
|
118
|
+
console.log(`[GatewayClient] Gateway handshake successful, protocol=${response.protocol} ` +
|
|
119
|
+
`(device=${this.deviceIdentity.deviceId.slice(0, 12)}...)`);
|
|
120
|
+
}
|
|
121
|
+
/** Wait for the gateway to emit a connect.challenge event with a nonce. */
|
|
122
|
+
waitForConnectChallenge() {
|
|
123
|
+
return new Promise((resolve, reject) => {
|
|
124
|
+
const handler = (event) => {
|
|
125
|
+
if (event?.event === 'connect.challenge') {
|
|
126
|
+
const nonce = event?.payload?.nonce;
|
|
127
|
+
if (typeof nonce !== 'string' || nonce.length === 0) {
|
|
128
|
+
reject(new Error('Gateway connect.challenge missing nonce'));
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
clearTimeout(timer);
|
|
132
|
+
resolve({ nonce });
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
// Reuse the existing request handler map by keying on event name.
|
|
136
|
+
this.requestHandlers.set('event:connect.challenge', {
|
|
137
|
+
resolve: (event) => handler(event),
|
|
138
|
+
reject,
|
|
139
|
+
});
|
|
140
|
+
const timer = setTimeout(() => {
|
|
141
|
+
this.requestHandlers.delete('event:connect.challenge');
|
|
142
|
+
reject(new Error(`Gateway connect.challenge timeout after ${CONNECT_CHALLENGE_TIMEOUT_MS}ms`));
|
|
143
|
+
}, CONNECT_CHALLENGE_TIMEOUT_MS);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Build the v3 device-auth payload and sign it with the device's Ed25519
|
|
148
|
+
* private key. Mirrors openclaw's `buildDeviceAuthPayloadV3` exactly so
|
|
149
|
+
* the gateway can verify the signature.
|
|
150
|
+
*/
|
|
151
|
+
buildDeviceConnectParams(params) {
|
|
152
|
+
const scopesCsv = SCOPES.join(',');
|
|
153
|
+
const signatureToken = this.config.gatewayToken ?? '';
|
|
154
|
+
const platform = (process.platform ?? '').toLowerCase();
|
|
155
|
+
const payload = [
|
|
156
|
+
'v3',
|
|
157
|
+
this.deviceIdentity.deviceId,
|
|
158
|
+
CLIENT_ID,
|
|
159
|
+
'node',
|
|
160
|
+
ROLE,
|
|
161
|
+
scopesCsv,
|
|
162
|
+
String(params.signedAt),
|
|
163
|
+
signatureToken,
|
|
164
|
+
params.nonce,
|
|
165
|
+
platform,
|
|
166
|
+
'', // deviceFamily — not used by node-host
|
|
167
|
+
].join('|');
|
|
168
|
+
const signature = this.deviceIdentity ? this.signDevicePayload(payload) : '';
|
|
169
|
+
return {
|
|
170
|
+
id: this.deviceIdentity.deviceId,
|
|
171
|
+
publicKey: publicKeyRawBase64UrlFromPem(this.deviceIdentity.publicKeyPem),
|
|
172
|
+
signature,
|
|
173
|
+
signedAt: params.signedAt,
|
|
174
|
+
nonce: params.nonce,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
signDevicePayload(payload) {
|
|
178
|
+
// We delegate to the DeviceIdentityStore's helper for testability and to
|
|
179
|
+
// keep all Ed25519 primitives in one place.
|
|
180
|
+
const store = new DeviceIdentityStore();
|
|
181
|
+
return store.signPayload(payload, this.deviceIdentity);
|
|
182
|
+
}
|
|
183
|
+
/** Persist newly issued deviceToken from hello-ok response. */
|
|
184
|
+
async persistHelloOk(payload) {
|
|
185
|
+
const auth = payload.auth;
|
|
186
|
+
if (!auth?.deviceToken) {
|
|
187
|
+
console.log('[GatewayClient] No deviceToken in hello-ok; using existing token (if any)');
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const stored = {
|
|
191
|
+
token: auth.deviceToken,
|
|
192
|
+
role: auth.role ?? ROLE,
|
|
193
|
+
scopes: Array.isArray(auth.scopes) && auth.scopes.length > 0
|
|
194
|
+
? auth.scopes
|
|
195
|
+
: SCOPES,
|
|
196
|
+
deviceId: this.deviceIdentity.deviceId,
|
|
197
|
+
updatedAt: new Date().toISOString(),
|
|
198
|
+
};
|
|
199
|
+
await this.deviceAuthStore.saveToken(stored);
|
|
200
|
+
}
|
|
201
|
+
handleMessage(message) {
|
|
202
|
+
if (message.type === 'res' && message.id) {
|
|
203
|
+
const handler = this.requestHandlers.get(message.id);
|
|
204
|
+
if (handler) {
|
|
205
|
+
if (message.ok) {
|
|
206
|
+
handler.resolve(message.payload);
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
handler.reject(new Error(JSON.stringify(message.error)));
|
|
210
|
+
}
|
|
211
|
+
this.requestHandlers.delete(message.id);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
else if (message.type === 'event') {
|
|
215
|
+
console.log(`[GatewayClient] ← event: ${message.event} payload=${JSON.stringify(message.payload).slice(0, 500)}`);
|
|
216
|
+
const handler = this.requestHandlers.get(`event:${message.event}`);
|
|
217
|
+
if (handler) {
|
|
218
|
+
handler.resolve(message);
|
|
219
|
+
// One-shot for connect.challenge: remove after dispatch.
|
|
220
|
+
if (message.event === 'connect.challenge') {
|
|
221
|
+
this.requestHandlers.delete(`event:${message.event}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
this.handleEvent(message);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
handleEvent(event) {
|
|
228
|
+
// 触发通用 '*' 订阅者
|
|
229
|
+
const wildcard = this.eventHandlers.get('*');
|
|
230
|
+
if (wildcard) {
|
|
231
|
+
try {
|
|
232
|
+
wildcard(event);
|
|
233
|
+
}
|
|
234
|
+
catch (err) {
|
|
235
|
+
console.error('[GatewayClient] event handler * threw:', err);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// 触发按 event 名订阅者
|
|
239
|
+
const named = this.eventHandlers.get(event?.event ?? '');
|
|
240
|
+
if (named) {
|
|
241
|
+
try {
|
|
242
|
+
named(event);
|
|
243
|
+
}
|
|
244
|
+
catch (err) {
|
|
245
|
+
console.error(`[GatewayClient] event handler ${event?.event} threw:`, err);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
/** Subscribe to gateway events. Pass '*' to receive all events. */
|
|
250
|
+
onEvent(eventName, handler) {
|
|
251
|
+
this.eventHandlers.set(eventName, handler);
|
|
252
|
+
}
|
|
253
|
+
offEvent(eventName) {
|
|
254
|
+
this.eventHandlers.delete(eventName);
|
|
255
|
+
}
|
|
256
|
+
sendRequest(method, params) {
|
|
257
|
+
return new Promise((resolve, reject) => {
|
|
258
|
+
const id = String(++this.requestIdCounter);
|
|
259
|
+
this.requestHandlers.set(id, { resolve, reject });
|
|
260
|
+
this.ws?.send(JSON.stringify({
|
|
261
|
+
type: 'req',
|
|
262
|
+
id,
|
|
263
|
+
method,
|
|
264
|
+
params,
|
|
265
|
+
}));
|
|
266
|
+
setTimeout(() => {
|
|
267
|
+
if (this.requestHandlers.has(id)) {
|
|
268
|
+
this.requestHandlers.delete(id);
|
|
269
|
+
reject(new Error(`Request timeout: ${method}`));
|
|
270
|
+
}
|
|
271
|
+
}, 30_000);
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
async agent(message, sessionId) {
|
|
275
|
+
// OpenClaw Gateway `agent` 协议要求:
|
|
276
|
+
// - 必传 idempotencyKey(用于去重,避免重复触发同一个 run)
|
|
277
|
+
// - 不允许 stream 字段(流式响应通过 event 通道单独推,不再走 req/res)
|
|
278
|
+
// - sessionKey 用于把 run 归类到同一个 session,会话续接时同一个 sessionId
|
|
279
|
+
// 必须使用同一个 sessionKey,否则会开新 session
|
|
280
|
+
const params = {
|
|
281
|
+
message,
|
|
282
|
+
idempotencyKey: randomUUID(),
|
|
283
|
+
timeout: 300,
|
|
284
|
+
};
|
|
285
|
+
if (sessionId) {
|
|
286
|
+
params.sessionId = sessionId;
|
|
287
|
+
params.sessionKey = `phoneclaw:${sessionId}`;
|
|
288
|
+
}
|
|
289
|
+
return this.sendRequest('agent', params);
|
|
290
|
+
}
|
|
291
|
+
async health() {
|
|
292
|
+
return this.sendRequest('health', {});
|
|
293
|
+
}
|
|
294
|
+
async status() {
|
|
295
|
+
return this.sendRequest('status', {});
|
|
296
|
+
}
|
|
297
|
+
async nodeList() {
|
|
298
|
+
return this.sendRequest('node.list', {});
|
|
299
|
+
}
|
|
300
|
+
async send(message) {
|
|
301
|
+
return this.sendRequest('send', { message });
|
|
302
|
+
}
|
|
303
|
+
get connected() {
|
|
304
|
+
return this.isConnected;
|
|
305
|
+
}
|
|
306
|
+
disconnect() {
|
|
307
|
+
if (this.ws) {
|
|
308
|
+
this.ws.close();
|
|
309
|
+
this.ws = null;
|
|
310
|
+
}
|
|
311
|
+
this.isConnected = false;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
//# sourceMappingURL=gateway.js.map
|