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 { 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,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
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('|');
|
||||
}
|
||||
64
package.json
64
package.json
@@ -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
2031
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user