feat: device identity for OpenClaw 2026.2.14+ pairing (#6)

- Generate Ed25519 keypair via Web Crypto API
- Persist keypair in IndexedDB (survives page reloads)
- Sign connect payload with device private key
- Include device object in connect params (id, publicKey, signature, signedAt)
- Handle NOT_PAIRED error with 'pairing' connection status
- Show pairing-pending banner with instructions to run openclaw devices approve
- Extract nonce from connect.challenge for v2 payload signing
- Add i18n translations for pairing banner in all 8 languages

Closes #6
This commit is contained in:
Nicolas Varrot
2026-02-16 00:00:20 +00:00
parent da75ac3ccc
commit 151215cd4b
9 changed files with 361 additions and 31 deletions

View File

@@ -2,6 +2,7 @@ import { useState, useEffect, useRef, useCallback } from 'react';
import { GatewayClient, type JsonPayload } from '../lib/gateway';
import { genIdempotencyKey } from '../lib/utils';
import { getStoredCredentials, storeCredentials, clearCredentials } from '../lib/credentials';
import { getOrCreateDeviceIdentity } from '../lib/deviceIdentity';
import { isSystemEvent } from '../lib/systemEvent';
import { getCachedMessages, setCachedMessages, mergeWithCache } from '../lib/messageCache';
import type { ChatMessage, MessageBlock, ConnectionStatus, Session, AgentIdentity } from '../types';
@@ -271,7 +272,7 @@ export function useGateway() {
}
}, []);
const setupClient = useCallback((wsUrl: string, token: string) => {
const setupClient = useCallback(async (wsUrl: string, token: string) => {
// Tear down existing client
if (clientRef.current) {
clientRef.current.disconnect();
@@ -280,6 +281,14 @@ export function useGateway() {
const client = new GatewayClient(wsUrl, token);
clientRef.current = client;
// Load device identity for signed connect handshake
try {
const identity = await getOrCreateDeviceIdentity();
client.setDeviceIdentity(identity);
} catch (err) {
console.warn('[PinchChat] Failed to load device identity, connecting without it:', err);
}
client.onStatus((s) => {
setStatus(s);
if (s === 'connected') {
@@ -291,6 +300,11 @@ export function useGateway() {
loadSessions();
loadAgentIdentity();
loadHistory(activeSessionRef.current);
} else if (s === 'pairing') {
setAuthenticated(true);
setConnectError(null);
setIsConnecting(false);
isConnectingRef.current = false;
} else if (s === 'disconnected' && !client.isConnected) {
// If we never connected successfully, this is an auth/connection error
if (isConnectingRef.current) {