- 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.
173 lines
4.5 KiB
TypeScript
173 lines
4.5 KiB
TypeScript
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('|');
|
|
}
|