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('|');
|
||||
}
|
||||
66
electron/utils/openclaw-auth.ts
Normal file
66
electron/utils/openclaw-auth.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { readFile, writeFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import { ensureDir, getOpenClawConfigDir } from './paths';
|
||||
|
||||
type OpenClawConfig = Record<string, unknown>;
|
||||
|
||||
const OPENCLAW_CONFIG_FILE_NAME = 'openclaw.json';
|
||||
|
||||
export function getOpenClawConfigPath(): string {
|
||||
return join(getOpenClawConfigDir(), OPENCLAW_CONFIG_FILE_NAME);
|
||||
}
|
||||
|
||||
async function readOpenClawConfig(): Promise<OpenClawConfig> {
|
||||
const configPath = getOpenClawConfigPath();
|
||||
|
||||
try {
|
||||
const raw = await readFile(configPath, 'utf-8');
|
||||
return JSON.parse(raw) as OpenClawConfig;
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
async function writeOpenClawConfig(config: OpenClawConfig): Promise<void> {
|
||||
ensureDir(getOpenClawConfigDir());
|
||||
await writeFile(getOpenClawConfigPath(), JSON.stringify(config, null, 2), 'utf-8');
|
||||
}
|
||||
|
||||
export async function syncBrowserConfigToOpenClaw(): Promise<void> {
|
||||
const config = await readOpenClawConfig();
|
||||
const browser = (
|
||||
config.browser && typeof config.browser === 'object'
|
||||
? { ...(config.browser as Record<string, unknown>) }
|
||||
: {}
|
||||
) as Record<string, unknown>;
|
||||
|
||||
let changed = false;
|
||||
|
||||
if (browser.enabled === undefined) {
|
||||
browser.enabled = true;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (browser.defaultProfile === undefined) {
|
||||
browser.defaultProfile = 'openclaw';
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (browser.ssrfPolicy == null) {
|
||||
browser.ssrfPolicy = { dangerouslyAllowPrivateNetwork: true };
|
||||
changed = true;
|
||||
} else if (
|
||||
typeof browser.ssrfPolicy === 'object'
|
||||
&& (browser.ssrfPolicy as Record<string, unknown>).dangerouslyAllowPrivateNetwork === undefined
|
||||
) {
|
||||
(browser.ssrfPolicy as Record<string, unknown>).dangerouslyAllowPrivateNetwork = true;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (!changed) {
|
||||
return;
|
||||
}
|
||||
|
||||
config.browser = browser;
|
||||
await writeOpenClawConfig(config);
|
||||
}
|
||||
@@ -64,6 +64,49 @@ export function getOpenClawEntryPath(): string {
|
||||
return join(getOpenClawDir(), OPENCLAW_ENTRY_FILE_NAME);
|
||||
}
|
||||
|
||||
export function getOpenClawNodeModulesDir(): string {
|
||||
return join(getOpenClawDir(), 'node_modules');
|
||||
}
|
||||
|
||||
export function getOpenClawBuildDir(): string {
|
||||
return join(app.getAppPath(), 'build', OPENCLAW_PACKAGE_DIR_NAME);
|
||||
}
|
||||
|
||||
export function getOpenClawPackageStatus(): {
|
||||
dir: string;
|
||||
entryPath: string;
|
||||
nodeModulesDir: string;
|
||||
packageExists: boolean;
|
||||
entryExists: boolean;
|
||||
nodeModulesExists: boolean;
|
||||
};
|
||||
|
||||
export function getOpenClawPackageStatus(
|
||||
overrides?: Partial<Pick<OpenClawRuntimePaths, 'dir' | 'entryPath'>>,
|
||||
): {
|
||||
dir: string;
|
||||
entryPath: string;
|
||||
nodeModulesDir: string;
|
||||
packageExists: boolean;
|
||||
entryExists: boolean;
|
||||
nodeModulesExists: boolean;
|
||||
} {
|
||||
const dir = overrides?.dir ?? getOpenClawDir();
|
||||
const entryPath = overrides?.entryPath ?? join(dir, OPENCLAW_ENTRY_FILE_NAME);
|
||||
const nodeModulesDir = join(dir, 'node_modules');
|
||||
const entryExists = existsSync(entryPath);
|
||||
const nodeModulesExists = existsSync(nodeModulesDir);
|
||||
|
||||
return {
|
||||
dir,
|
||||
entryPath,
|
||||
nodeModulesDir,
|
||||
packageExists: existsSync(dir),
|
||||
entryExists,
|
||||
nodeModulesExists,
|
||||
};
|
||||
}
|
||||
|
||||
export function getClawHubCliBinPath(): string {
|
||||
const binName = process.platform === 'win32' ? 'clawhub.cmd' : 'clawhub';
|
||||
return join(app.getAppPath(), 'node_modules', '.bin', binName);
|
||||
|
||||
Reference in New Issue
Block a user