feat: enhance after-pack script to copy OpenClaw runtime dependencies
- Added a new script `bundle-openclaw.mjs` to bundle OpenClaw runtime dependencies. - Updated `after-pack.cjs` to copy bundled OpenClaw runtime and its node_modules. - Improved cleanup of unnecessary development files in node_modules. - Adjusted paths for resources in the packaging process. style: update loading indicator styles in ChatHistoryPanel - Changed the border radius and padding for the loading indicator in ChatHistoryPanel. fix: improve ProvidersSection to handle provider account syncing - Added logic to sync model configuration to provider accounts. - Introduced error handling and loading states during the sync process. - Enhanced vendor resolution and account management logic. fix: fallback session handling in chat store - Implemented fallback session logic in loadSessions to ensure a valid session is always available on error.
This commit is contained in:
172
electron/utils/device-identity.ts
Normal file
172
electron/utils/device-identity.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
import crypto from 'crypto';
|
||||
import { access, chmod, constants, mkdir, readFile, writeFile } from 'fs/promises';
|
||||
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(buffer: Buffer): string {
|
||||
return buffer
|
||||
.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');
|
||||
}
|
||||
|
||||
async function fileExists(filePath: string): Promise<boolean> {
|
||||
try {
|
||||
await access(filePath, constants.F_OK);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function generateIdentity(): Promise<DeviceIdentity> {
|
||||
const { publicKey, privateKey } = await new Promise<crypto.KeyPairKeyObjectResult>(
|
||||
(resolve, reject) => {
|
||||
crypto.generateKeyPair('ed25519', (error, nextPublicKey, nextPrivateKey) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
resolve({
|
||||
publicKey: nextPublicKey,
|
||||
privateKey: nextPrivateKey,
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
deviceId: fingerprintPublicKey(publicKey.export({ type: 'spki', format: 'pem' }).toString()),
|
||||
publicKeyPem: publicKey.export({ type: 'spki', format: 'pem' }).toString(),
|
||||
privateKeyPem: privateKey.export({ type: 'pkcs8', format: 'pem' }).toString(),
|
||||
};
|
||||
}
|
||||
|
||||
export async function loadOrCreateDeviceIdentity(filePath: string): Promise<DeviceIdentity> {
|
||||
try {
|
||||
if (await fileExists(filePath)) {
|
||||
const raw = await readFile(filePath, 'utf-8');
|
||||
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 };
|
||||
await writeFile(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 and create a fresh identity.
|
||||
}
|
||||
|
||||
const identity = await generateIdentity();
|
||||
const dir = path.dirname(filePath);
|
||||
|
||||
if (!(await fileExists(dir))) {
|
||||
await mkdir(dir, { recursive: true });
|
||||
}
|
||||
|
||||
await writeFile(filePath, `${JSON.stringify({
|
||||
version: 1,
|
||||
...identity,
|
||||
createdAtMs: Date.now(),
|
||||
}, null, 2)}\n`, { mode: 0o600 });
|
||||
|
||||
try {
|
||||
await chmod(filePath, 0o600);
|
||||
} catch {
|
||||
// Ignore chmod failures on unsupported filesystems.
|
||||
}
|
||||
|
||||
return identity;
|
||||
}
|
||||
|
||||
export function signDevicePayload(privateKeyPem: string, payload: string): string {
|
||||
const key = crypto.createPrivateKey(privateKeyPem);
|
||||
return base64UrlEncode(crypto.sign(null, Buffer.from(payload, 'utf-8'), key));
|
||||
}
|
||||
|
||||
export function publicKeyRawBase64UrlFromPem(publicKeyPem: string): string {
|
||||
return base64UrlEncode(derivePublicKeyRaw(publicKeyPem));
|
||||
}
|
||||
|
||||
export function buildDeviceAuthPayload(params: DeviceAuthPayloadParams): string {
|
||||
const version = params.version ?? (params.nonce ? 'v2' : 'v1');
|
||||
const scopes = params.scopes.join(',');
|
||||
const token = params.token ?? '';
|
||||
const payload = [
|
||||
version,
|
||||
params.deviceId,
|
||||
params.clientId,
|
||||
params.clientMode,
|
||||
params.role,
|
||||
scopes,
|
||||
String(params.signedAtMs),
|
||||
token,
|
||||
];
|
||||
|
||||
if (version === 'v2') {
|
||||
payload.push(params.nonce ?? '');
|
||||
}
|
||||
|
||||
return payload.join('|');
|
||||
}
|
||||
Reference in New Issue
Block a user