chore(deps): update dependencies and devDependencies in package.json (#100)
This commit is contained in:
@@ -22,6 +22,13 @@ import { GatewayEventType, JsonRpcNotification, isNotification, isResponse } fro
|
|||||||
import { logger } from '../utils/logger';
|
import { logger } from '../utils/logger';
|
||||||
import { getUvMirrorEnv } from '../utils/uv-env';
|
import { getUvMirrorEnv } from '../utils/uv-env';
|
||||||
import { isPythonReady, setupManagedPython } from '../utils/uv-setup';
|
import { isPythonReady, setupManagedPython } from '../utils/uv-setup';
|
||||||
|
import {
|
||||||
|
loadOrCreateDeviceIdentity,
|
||||||
|
signDevicePayload,
|
||||||
|
publicKeyRawBase64UrlFromPem,
|
||||||
|
buildDeviceAuthPayload,
|
||||||
|
type DeviceIdentity,
|
||||||
|
} from '../utils/device-identity';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gateway connection status
|
* Gateway connection status
|
||||||
@@ -120,10 +127,22 @@ export class GatewayManager extends EventEmitter {
|
|||||||
reject: (error: Error) => void;
|
reject: (error: Error) => void;
|
||||||
timeout: NodeJS.Timeout;
|
timeout: NodeJS.Timeout;
|
||||||
}> = new Map();
|
}> = new Map();
|
||||||
|
private deviceIdentity: DeviceIdentity | null = null;
|
||||||
|
|
||||||
constructor(config?: Partial<ReconnectConfig>) {
|
constructor(config?: Partial<ReconnectConfig>) {
|
||||||
super();
|
super();
|
||||||
this.reconnectConfig = { ...DEFAULT_RECONNECT_CONFIG, ...config };
|
this.reconnectConfig = { ...DEFAULT_RECONNECT_CONFIG, ...config };
|
||||||
|
this.initDeviceIdentity();
|
||||||
|
}
|
||||||
|
|
||||||
|
private initDeviceIdentity(): void {
|
||||||
|
try {
|
||||||
|
const identityPath = path.join(app.getPath('userData'), 'clawx-device-identity.json');
|
||||||
|
this.deviceIdentity = loadOrCreateDeviceIdentity(identityPath);
|
||||||
|
logger.debug(`Device identity loaded (deviceId=${this.deviceIdentity.deviceId})`);
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn('Failed to load device identity, scopes will be limited:', err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private sanitizeSpawnArgs(args: string[]): string[] {
|
private sanitizeSpawnArgs(args: string[]): string[] {
|
||||||
@@ -757,7 +776,34 @@ export class GatewayManager extends EventEmitter {
|
|||||||
|
|
||||||
// Send proper connect handshake as required by OpenClaw Gateway protocol
|
// Send proper connect handshake as required by OpenClaw Gateway protocol
|
||||||
// The Gateway expects: { type: "req", id: "...", method: "connect", params: ConnectParams }
|
// The Gateway expects: { type: "req", id: "...", method: "connect", params: ConnectParams }
|
||||||
|
// Since 2026.2.15, scopes are only granted when a signed device identity is included.
|
||||||
connectId = `connect-${Date.now()}`;
|
connectId = `connect-${Date.now()}`;
|
||||||
|
const role = 'operator';
|
||||||
|
const scopes = ['operator.admin'];
|
||||||
|
const signedAtMs = Date.now();
|
||||||
|
const clientId = 'gateway-client';
|
||||||
|
const clientMode = 'ui';
|
||||||
|
|
||||||
|
const device = (() => {
|
||||||
|
if (!this.deviceIdentity) return undefined;
|
||||||
|
const payload = buildDeviceAuthPayload({
|
||||||
|
deviceId: this.deviceIdentity.deviceId,
|
||||||
|
clientId,
|
||||||
|
clientMode,
|
||||||
|
role,
|
||||||
|
scopes,
|
||||||
|
signedAtMs,
|
||||||
|
token: gatewayToken ?? null,
|
||||||
|
});
|
||||||
|
const signature = signDevicePayload(this.deviceIdentity.privateKeyPem, payload);
|
||||||
|
return {
|
||||||
|
id: this.deviceIdentity.deviceId,
|
||||||
|
publicKey: publicKeyRawBase64UrlFromPem(this.deviceIdentity.publicKeyPem),
|
||||||
|
signature,
|
||||||
|
signedAt: signedAtMs,
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
const connectFrame = {
|
const connectFrame = {
|
||||||
type: 'req',
|
type: 'req',
|
||||||
id: connectId,
|
id: connectId,
|
||||||
@@ -766,18 +812,19 @@ export class GatewayManager extends EventEmitter {
|
|||||||
minProtocol: 3,
|
minProtocol: 3,
|
||||||
maxProtocol: 3,
|
maxProtocol: 3,
|
||||||
client: {
|
client: {
|
||||||
id: 'gateway-client',
|
id: clientId,
|
||||||
displayName: 'ClawX',
|
displayName: 'ClawX',
|
||||||
version: '0.1.0',
|
version: '0.1.0',
|
||||||
platform: process.platform,
|
platform: process.platform,
|
||||||
mode: 'ui',
|
mode: clientMode,
|
||||||
},
|
},
|
||||||
auth: {
|
auth: {
|
||||||
token: gatewayToken,
|
token: gatewayToken,
|
||||||
},
|
},
|
||||||
caps: [],
|
caps: [],
|
||||||
role: 'operator',
|
role,
|
||||||
scopes: [],
|
scopes,
|
||||||
|
device,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
128
electron/utils/device-identity.ts
Normal file
128
electron/utils/device-identity.ts
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
/**
|
||||||
|
* Device identity utilities for OpenClaw Gateway authentication.
|
||||||
|
*
|
||||||
|
* OpenClaw Gateway 2026.2.15+ requires a signed device identity in the
|
||||||
|
* connect handshake to grant scopes (operator.read, operator.write, etc.).
|
||||||
|
* Without a device, the gateway strips all requested scopes.
|
||||||
|
*/
|
||||||
|
import crypto from 'crypto';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
export interface DeviceIdentity {
|
||||||
|
deviceId: string;
|
||||||
|
publicKeyPem: string;
|
||||||
|
privateKeyPem: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeviceAuthPayloadParams {
|
||||||
|
deviceId: string;
|
||||||
|
clientId: string;
|
||||||
|
clientMode: string;
|
||||||
|
role: string;
|
||||||
|
scopes: string[];
|
||||||
|
signedAtMs: number;
|
||||||
|
token?: string | null;
|
||||||
|
nonce?: string | null;
|
||||||
|
version?: 'v1' | 'v2';
|
||||||
|
}
|
||||||
|
|
||||||
|
const ED25519_SPKI_PREFIX = Buffer.from('302a300506032b6570032100', 'hex');
|
||||||
|
|
||||||
|
function base64UrlEncode(buf: Buffer): string {
|
||||||
|
return buf.toString('base64').replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function derivePublicKeyRaw(publicKeyPem: string): Buffer {
|
||||||
|
const spki = crypto.createPublicKey(publicKeyPem).export({ type: 'spki', format: 'der' }) as Buffer;
|
||||||
|
if (
|
||||||
|
spki.length === ED25519_SPKI_PREFIX.length + 32 &&
|
||||||
|
spki.subarray(0, ED25519_SPKI_PREFIX.length).equals(ED25519_SPKI_PREFIX)
|
||||||
|
) {
|
||||||
|
return spki.subarray(ED25519_SPKI_PREFIX.length);
|
||||||
|
}
|
||||||
|
return spki;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fingerprintPublicKey(publicKeyPem: string): string {
|
||||||
|
const raw = derivePublicKeyRaw(publicKeyPem);
|
||||||
|
return crypto.createHash('sha256').update(raw).digest('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateIdentity(): DeviceIdentity {
|
||||||
|
const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519');
|
||||||
|
const publicKeyPem = (publicKey.export({ type: 'spki', format: 'pem' }) as Buffer).toString();
|
||||||
|
const privateKeyPem = (privateKey.export({ type: 'pkcs8', format: 'pem' }) as Buffer).toString();
|
||||||
|
return {
|
||||||
|
deviceId: fingerprintPublicKey(publicKeyPem),
|
||||||
|
publicKeyPem,
|
||||||
|
privateKeyPem,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load device identity from disk, or create and persist a new one.
|
||||||
|
* The identity file is stored at `filePath` with mode 0o600.
|
||||||
|
*/
|
||||||
|
export function loadOrCreateDeviceIdentity(filePath: string): DeviceIdentity {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
const raw = fs.readFileSync(filePath, 'utf8');
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
if (
|
||||||
|
parsed?.version === 1 &&
|
||||||
|
typeof parsed.deviceId === 'string' &&
|
||||||
|
typeof parsed.publicKeyPem === 'string' &&
|
||||||
|
typeof parsed.privateKeyPem === 'string'
|
||||||
|
) {
|
||||||
|
const derivedId = fingerprintPublicKey(parsed.publicKeyPem);
|
||||||
|
if (derivedId && derivedId !== parsed.deviceId) {
|
||||||
|
const updated = { ...parsed, deviceId: derivedId };
|
||||||
|
fs.writeFileSync(filePath, `${JSON.stringify(updated, null, 2)}\n`, { mode: 0o600 });
|
||||||
|
return { deviceId: derivedId, publicKeyPem: parsed.publicKeyPem, privateKeyPem: parsed.privateKeyPem };
|
||||||
|
}
|
||||||
|
return { deviceId: parsed.deviceId, publicKeyPem: parsed.publicKeyPem, privateKeyPem: parsed.privateKeyPem };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// fall through to create a new identity
|
||||||
|
}
|
||||||
|
|
||||||
|
const identity = generateIdentity();
|
||||||
|
const dir = path.dirname(filePath);
|
||||||
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||||
|
const stored = { version: 1, ...identity, createdAtMs: Date.now() };
|
||||||
|
fs.writeFileSync(filePath, `${JSON.stringify(stored, null, 2)}\n`, { mode: 0o600 });
|
||||||
|
try { fs.chmodSync(filePath, 0o600); } catch { /* ignore */ }
|
||||||
|
return identity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sign a string payload with the Ed25519 private key, returns base64url signature. */
|
||||||
|
export function signDevicePayload(privateKeyPem: string, payload: string): string {
|
||||||
|
const key = crypto.createPrivateKey(privateKeyPem);
|
||||||
|
return base64UrlEncode(crypto.sign(null, Buffer.from(payload, 'utf8'), key));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Encode the raw Ed25519 public key bytes (from PEM) as base64url. */
|
||||||
|
export function publicKeyRawBase64UrlFromPem(publicKeyPem: string): string {
|
||||||
|
return base64UrlEncode(derivePublicKeyRaw(publicKeyPem));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Build the canonical payload string that must be signed for device auth. */
|
||||||
|
export function buildDeviceAuthPayload(params: DeviceAuthPayloadParams): string {
|
||||||
|
const version = params.version ?? (params.nonce ? 'v2' : 'v1');
|
||||||
|
const scopes = params.scopes.join(',');
|
||||||
|
const token = params.token ?? '';
|
||||||
|
const base = [
|
||||||
|
version,
|
||||||
|
params.deviceId,
|
||||||
|
params.clientId,
|
||||||
|
params.clientMode,
|
||||||
|
params.role,
|
||||||
|
scopes,
|
||||||
|
String(params.signedAtMs),
|
||||||
|
token,
|
||||||
|
];
|
||||||
|
if (version === 'v2') base.push(params.nonce ?? '');
|
||||||
|
return base.join('|');
|
||||||
|
}
|
||||||
62
package.json
62
package.json
@@ -42,27 +42,27 @@
|
|||||||
"postversion": "git push && git push --tags"
|
"postversion": "git push && git push --tags"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-dialog": "^1.1.4",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.4",
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
"@radix-ui/react-label": "^2.1.1",
|
"@radix-ui/react-label": "^2.1.8",
|
||||||
"@radix-ui/react-progress": "^1.1.1",
|
"@radix-ui/react-progress": "^1.1.8",
|
||||||
"@radix-ui/react-radio-group": "^1.2.2",
|
"@radix-ui/react-radio-group": "^1.3.8",
|
||||||
"@radix-ui/react-select": "^2.1.4",
|
"@radix-ui/react-select": "^2.2.6",
|
||||||
"@radix-ui/react-separator": "^1.1.1",
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
"@radix-ui/react-slot": "^1.1.1",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@radix-ui/react-switch": "^1.1.2",
|
"@radix-ui/react-switch": "^1.2.6",
|
||||||
"@radix-ui/react-tabs": "^1.1.2",
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
"@radix-ui/react-toast": "^1.2.4",
|
"@radix-ui/react-toast": "^1.2.15",
|
||||||
"@radix-ui/react-tooltip": "^1.1.6",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clawhub": "^0.5.0",
|
"clawhub": "^0.5.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"electron-store": "^11.0.2",
|
"electron-store": "^11.0.2",
|
||||||
"electron-updater": "^6.8.2",
|
"electron-updater": "^6.8.3",
|
||||||
"framer-motion": "^12.33.0",
|
"framer-motion": "^12.34.1",
|
||||||
"i18next": "^25.8.4",
|
"i18next": "^25.8.10",
|
||||||
"lucide-react": "^0.563.0",
|
"lucide-react": "^0.563.0",
|
||||||
"openclaw": "2026.2.6-3",
|
"openclaw": "2026.2.15",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
"react-i18next": "^16.5.4",
|
"react-i18next": "^16.5.4",
|
||||||
@@ -70,36 +70,36 @@
|
|||||||
"react-router-dom": "^7.13.0",
|
"react-router-dom": "^7.13.0",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.1",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"ws": "^8.19.0",
|
"ws": "^8.19.0",
|
||||||
"zustand": "^5.0.11"
|
"zustand": "^5.0.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
"@playwright/test": "^1.49.1",
|
"@playwright/test": "^1.58.2",
|
||||||
"@testing-library/jest-dom": "^6.9.1",
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
"@testing-library/react": "^16.1.0",
|
"@testing-library/react": "^16.3.2",
|
||||||
"@types/node": "^25.2.1",
|
"@types/node": "^25.2.3",
|
||||||
"@types/react": "^19.2.13",
|
"@types/react": "^19.2.14",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@types/ws": "^8.5.13",
|
"@types/ws": "^8.18.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.54.0",
|
"@typescript-eslint/eslint-plugin": "^8.56.0",
|
||||||
"@typescript-eslint/parser": "^8.54.0",
|
"@typescript-eslint/parser": "^8.56.0",
|
||||||
"@vitejs/plugin-react": "^5.1.3",
|
"@vitejs/plugin-react": "^5.1.4",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.24",
|
||||||
"electron": "^40.2.1",
|
"electron": "^40.4.1",
|
||||||
"electron-builder": "^26.7.0",
|
"electron-builder": "^26.7.0",
|
||||||
"eslint": "^10.0.0",
|
"eslint": "^10.0.0",
|
||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
"eslint-plugin-react-refresh": "^0.5.0",
|
"eslint-plugin-react-refresh": "^0.5.0",
|
||||||
"globals": "^17.3.0",
|
"globals": "^17.3.0",
|
||||||
"jsdom": "^28.0.0",
|
"jsdom": "^28.1.0",
|
||||||
"png2icons": "^2.0.1",
|
"png2icons": "^2.0.1",
|
||||||
"postcss": "^8.4.49",
|
"postcss": "^8.5.6",
|
||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.19",
|
||||||
"typescript": "^5.7.2",
|
"typescript": "^5.9.3",
|
||||||
"vite": "^7.3.1",
|
"vite": "^7.3.1",
|
||||||
"vite-plugin-electron": "^0.29.0",
|
"vite-plugin-electron": "^0.29.0",
|
||||||
"vite-plugin-electron-renderer": "^0.14.6",
|
"vite-plugin-electron-renderer": "^0.14.6",
|
||||||
|
|||||||
2031
pnpm-lock.yaml
generated
2031
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user