chore(deps): update dependencies and devDependencies in package.json (#100)

This commit is contained in:
Haze
2026-02-18 20:28:42 +08:00
committed by GitHub
Unverified
parent 26ce009a41
commit 5005d405d9
4 changed files with 1270 additions and 1008 deletions

View File

@@ -22,6 +22,13 @@ import { GatewayEventType, JsonRpcNotification, isNotification, isResponse } fro
import { logger } from '../utils/logger';
import { getUvMirrorEnv } from '../utils/uv-env';
import { isPythonReady, setupManagedPython } from '../utils/uv-setup';
import {
loadOrCreateDeviceIdentity,
signDevicePayload,
publicKeyRawBase64UrlFromPem,
buildDeviceAuthPayload,
type DeviceIdentity,
} from '../utils/device-identity';
/**
* Gateway connection status
@@ -120,10 +127,22 @@ export class GatewayManager extends EventEmitter {
reject: (error: Error) => void;
timeout: NodeJS.Timeout;
}> = new Map();
private deviceIdentity: DeviceIdentity | null = null;
constructor(config?: Partial<ReconnectConfig>) {
super();
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[] {
@@ -757,7 +776,34 @@ export class GatewayManager extends EventEmitter {
// Send proper connect handshake as required by OpenClaw Gateway protocol
// 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()}`;
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 = {
type: 'req',
id: connectId,
@@ -766,18 +812,19 @@ export class GatewayManager extends EventEmitter {
minProtocol: 3,
maxProtocol: 3,
client: {
id: 'gateway-client',
id: clientId,
displayName: 'ClawX',
version: '0.1.0',
platform: process.platform,
mode: 'ui',
mode: clientMode,
},
auth: {
token: gatewayToken,
},
caps: [],
role: 'operator',
scopes: [],
role,
scopes,
device,
},
};

View 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('|');
}

View File

@@ -42,27 +42,27 @@
"postversion": "git push && git push --tags"
},
"dependencies": {
"@radix-ui/react-dialog": "^1.1.4",
"@radix-ui/react-dropdown-menu": "^2.1.4",
"@radix-ui/react-label": "^2.1.1",
"@radix-ui/react-progress": "^1.1.1",
"@radix-ui/react-radio-group": "^1.2.2",
"@radix-ui/react-select": "^2.1.4",
"@radix-ui/react-separator": "^1.1.1",
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-switch": "^1.1.2",
"@radix-ui/react-tabs": "^1.1.2",
"@radix-ui/react-toast": "^1.2.4",
"@radix-ui/react-tooltip": "^1.1.6",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-progress": "^1.1.8",
"@radix-ui/react-radio-group": "^1.3.8",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-toast": "^1.2.15",
"@radix-ui/react-tooltip": "^1.2.8",
"class-variance-authority": "^0.7.1",
"clawhub": "^0.5.0",
"clsx": "^2.1.1",
"electron-store": "^11.0.2",
"electron-updater": "^6.8.2",
"framer-motion": "^12.33.0",
"i18next": "^25.8.4",
"electron-updater": "^6.8.3",
"framer-motion": "^12.34.1",
"i18next": "^25.8.10",
"lucide-react": "^0.563.0",
"openclaw": "2026.2.6-3",
"openclaw": "2026.2.15",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-i18next": "^16.5.4",
@@ -70,36 +70,36 @@
"react-router-dom": "^7.13.0",
"remark-gfm": "^4.0.1",
"sonner": "^2.0.7",
"tailwind-merge": "^3.4.0",
"tailwind-merge": "^3.4.1",
"tailwindcss-animate": "^1.0.7",
"ws": "^8.19.0",
"zustand": "^5.0.11"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@playwright/test": "^1.49.1",
"@playwright/test": "^1.58.2",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.1.0",
"@types/node": "^25.2.1",
"@types/react": "^19.2.13",
"@testing-library/react": "^16.3.2",
"@types/node": "^25.2.3",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@types/ws": "^8.5.13",
"@typescript-eslint/eslint-plugin": "^8.54.0",
"@typescript-eslint/parser": "^8.54.0",
"@vitejs/plugin-react": "^5.1.3",
"autoprefixer": "^10.4.20",
"electron": "^40.2.1",
"@types/ws": "^8.18.1",
"@typescript-eslint/eslint-plugin": "^8.56.0",
"@typescript-eslint/parser": "^8.56.0",
"@vitejs/plugin-react": "^5.1.4",
"autoprefixer": "^10.4.24",
"electron": "^40.4.1",
"electron-builder": "^26.7.0",
"eslint": "^10.0.0",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.5.0",
"globals": "^17.3.0",
"jsdom": "^28.0.0",
"jsdom": "^28.1.0",
"png2icons": "^2.0.1",
"postcss": "^8.4.49",
"postcss": "^8.5.6",
"sharp": "^0.34.5",
"tailwindcss": "^3.4.17",
"typescript": "^5.7.2",
"tailwindcss": "^3.4.19",
"typescript": "^5.9.3",
"vite": "^7.3.1",
"vite-plugin-electron": "^0.29.0",
"vite-plugin-electron-renderer": "^0.14.6",
@@ -107,4 +107,4 @@
"zx": "^8.8.5"
},
"packageManager": "pnpm@10.29.2+sha512.bef43fa759d91fd2da4b319a5a0d13ef7a45bb985a3d7342058470f9d2051a3ba8674e629672654686ef9443ad13a82da2beb9eeb3e0221c87b8154fff9d74b8"
}
}

2031
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff