openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
252 lines (251 loc) • 12.3 kB
JavaScript
import { f as asSafeIntegerInRange } from "./number-coercion-CkhMiRw8.mjs";
import { t as hasErrnoCode } from "./errno-FR4CRyya.mjs";
import { a as executeSqliteQueryTakeFirstSync, i as executeSqliteQuerySync, o as getNodeSqliteKysely } from "./kysely-sync-DUWzQvCU.mjs";
import { n as StartupMaintenanceRequiredError } from "./startup-maintenance-required-5wJsitFV.mjs";
import { c as resolveOpenClawStateSqlitePath, o as resolveOpenClawStateDirForDatabasePath } from "./openclaw-state-db.paths-CmKkMYdo.mjs";
import { l as runOpenClawStateWriteTransaction, r as openOpenClawStateDatabase } from "./openclaw-state-db-DJZ7EKoI.mjs";
import { u as withExistingOpenClawStateDatabaseReadOnly } from "./openclaw-state-db-readonly-ywwvuxdQ.mjs";
import { n as pathMayExistSync } from "./path-existence-B679BE7y.mjs";
import { n as deriveCanonicalEd25519PrivateKeyRaw, r as deriveCanonicalEd25519PublicKeyRaw } from "./ed25519-signature-DxLj6ug0.mjs";
import path from "node:path";
import crypto from "node:crypto";
//#region src/infra/device-identity-store.ts
const PRIMARY_DEVICE_IDENTITY_KEY = "primary";
var DeviceIdentityStorageError = class extends Error {
constructor(message, options) {
super(message, options);
this.name = "DeviceIdentityStorageError";
}
};
function normalizeIdentityKey(key) {
const normalized = key ?? PRIMARY_DEVICE_IDENTITY_KEY;
if (normalized.length === 0 || normalized !== normalized.trim()) throw new DeviceIdentityStorageError("Device identity key must be a non-empty string without surrounding whitespace.");
if (normalized.length > 128) throw new DeviceIdentityStorageError("Device identity key exceeds 128 characters.");
return normalized;
}
function invalidStoredIdentityError(identityKey, cause) {
return new DeviceIdentityStorageError(`SQLite contains an invalid persisted device identity "${identityKey}". Run "openclaw doctor --fix" before starting the gateway or connecting this client.`, cause === void 0 ? void 0 : { cause });
}
function fingerprintPublicKey(publicKeyPem) {
const raw = deriveCanonicalEd25519PublicKeyRaw(publicKeyPem);
return crypto.createHash("sha256").update(raw).digest("hex");
}
/** Generate canonical Ed25519 material before entering a synchronous write transaction. */
function generateStoredDeviceIdentity(now = Date.now()) {
const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");
const publicKeyPem = publicKey.export({
type: "spki",
format: "pem"
});
const privateKeyPem = privateKey.export({
type: "pkcs8",
format: "pem"
});
return {
deviceId: fingerprintPublicKey(publicKeyPem),
publicKeyPem,
privateKeyPem,
createdAtMs: now
};
}
function keyPairMatches(publicKeyPem, privateKeyPem) {
try {
deriveCanonicalEd25519PublicKeyRaw(publicKeyPem);
deriveCanonicalEd25519PrivateKeyRaw(privateKeyPem);
return crypto.createPublicKey(privateKeyPem).export({
type: "spki",
format: "pem"
}) === publicKeyPem;
} catch {
return false;
}
}
function parseCreatedAtMs(value) {
return asSafeIntegerInRange(value, { min: 0 }) ?? null;
}
/** Validate persisted key material and return the canonical runtime shape. */
function validateStoredDeviceIdentity(value, identityKey = PRIMARY_DEVICE_IDENTITY_KEY) {
try {
if (!value.deviceId || !/^[a-f0-9]{64}$/.test(value.deviceId) || !value.publicKeyPem || !value.privateKeyPem || parseCreatedAtMs(value.createdAtMs) === null || !keyPairMatches(value.publicKeyPem, value.privateKeyPem)) throw invalidStoredIdentityError(identityKey);
if (fingerprintPublicKey(value.publicKeyPem) !== value.deviceId) throw invalidStoredIdentityError(identityKey);
return {
deviceId: value.deviceId,
publicKeyPem: value.publicKeyPem,
privateKeyPem: value.privateKeyPem
};
} catch (error) {
if (error instanceof DeviceIdentityStorageError) throw error;
throw invalidStoredIdentityError(identityKey, error);
}
}
function rowToStoredIdentity(row, expectedIdentityKey) {
if (row.identity_key !== expectedIdentityKey || typeof row.device_id !== "string" || typeof row.public_key_pem !== "string" || typeof row.private_key_pem !== "string" || parseCreatedAtMs(row.created_at_ms) === null || parseCreatedAtMs(row.updated_at_ms) === null) throw invalidStoredIdentityError(expectedIdentityKey);
return {
deviceId: row.device_id,
publicKeyPem: row.public_key_pem,
privateKeyPem: row.private_key_pem,
createdAtMs: row.created_at_ms
};
}
function salvageStoredIdentityRow(row, expectedIdentityKey, repairedAtMs) {
if (row.identity_key !== expectedIdentityKey || typeof row.public_key_pem !== "string" || typeof row.private_key_pem !== "string") return null;
try {
const publicKey = crypto.createPublicKey(row.public_key_pem);
const privateKey = crypto.createPrivateKey(row.private_key_pem);
if (publicKey.asymmetricKeyType !== "ed25519" || privateKey.asymmetricKeyType !== "ed25519") return null;
const canonicalPublicKeyPem = publicKey.export({
type: "spki",
format: "pem"
});
const canonicalPrivateKeyPem = privateKey.export({
type: "pkcs8",
format: "pem"
});
if (crypto.createPublicKey(canonicalPrivateKeyPem).export({
type: "spki",
format: "pem"
}) !== canonicalPublicKeyPem) return null;
const createdAtMs = parseCreatedAtMs(row.created_at_ms) ?? parseCreatedAtMs(row.updated_at_ms) ?? repairedAtMs;
const salvaged = {
deviceId: fingerprintPublicKey(canonicalPublicKeyPem),
publicKeyPem: canonicalPublicKeyPem,
privateKeyPem: canonicalPrivateKeyPem,
createdAtMs
};
validateStoredDeviceIdentity(salvaged, expectedIdentityKey);
return salvaged;
} catch {
return null;
}
}
function storedIdentityToRow(identityKey, stored, updatedAtMs = stored.createdAtMs) {
return {
identity_key: identityKey,
device_id: stored.deviceId,
public_key_pem: stored.publicKeyPem,
private_key_pem: stored.privateKeyPem,
created_at_ms: stored.createdAtMs,
updated_at_ms: updatedAtMs
};
}
function readStoredIdentityRowFromDatabase(database, identityKey) {
const db = getNodeSqliteKysely(database.db);
return executeSqliteQueryTakeFirstSync(database.db, db.selectFrom("device_identities").selectAll().where("identity_key", "=", identityKey)) ?? null;
}
function readStoredIdentityFromDatabase(database, identityKey) {
const row = readStoredIdentityRowFromDatabase(database, identityKey);
if (!row) return null;
const stored = rowToStoredIdentity(row, identityKey);
validateStoredDeviceIdentity(stored, identityKey);
return stored;
}
function isEmptyBootstrapIdentityTableMiss(database, error) {
if (!(error instanceof Error) || !hasErrnoCode(error, "ERR_SQLITE_ERROR") || !/\bno such table: device_identities\b/iu.test(error.message)) return false;
const db = getNodeSqliteKysely(database.db);
return !executeSqliteQueryTakeFirstSync(database.db, db.selectFrom("sqlite_master").select("name").where("name", "not like", "sqlite_%").limit(1));
}
/** Resolve the concrete database and row identity used by process caches and diagnostics. */
function resolveDeviceIdentityStore(options = {}) {
return {
databasePath: path.resolve(options.path ?? resolveOpenClawStateSqlitePath(options.env ?? process.env)),
identityKey: normalizeIdentityKey(options.identityKey)
};
}
function assertNoPendingLegacyIdentity(options) {
const { databasePath, identityKey } = resolveDeviceIdentityStore(options);
if (identityKey !== PRIMARY_DEVICE_IDENTITY_KEY) return;
const legacyPath = path.join(resolveOpenClawStateDirForDatabasePath(databasePath), "identity", "device.json");
if (pathMayExistSync(`${legacyPath}.doctor-importing`) || pathMayExistSync(`${legacyPath}.native-importing`) || pathMayExistSync(legacyPath)) throw new StartupMaintenanceRequiredError("state-migrations", `Legacy device identity exists at ${legacyPath}. Run "openclaw doctor --fix" before starting the gateway or connecting this client.`);
}
/** Read through the writable shared-state lifecycle, validating any existing row. */
function readStoredDeviceIdentity(options = {}) {
const resolved = resolveDeviceIdentityStore(options);
return readStoredIdentityFromDatabase(openOpenClawStateDatabase({
env: options.env,
path: resolved.databasePath
}), resolved.identityKey);
}
/** Read identity rows without creating identities or joining the writer lifecycle. */
function readStoredDeviceIdentityReadOnly(options = {}) {
const resolved = resolveDeviceIdentityStore(options);
return withExistingOpenClawStateDatabaseReadOnly((database) => {
try {
return readStoredIdentityFromDatabase(database, resolved.identityKey);
} catch (error) {
if (isEmptyBootstrapIdentityTableMiss(database, error)) return null;
throw error;
}
}, {
env: options.env,
path: resolved.databasePath
}) ?? null;
}
/** Insert a candidate only when the key is still absent, then return the authoritative row. */
function insertStoredDeviceIdentityIfAbsent(candidate, options = {}) {
const resolved = resolveDeviceIdentityStore(options);
validateStoredDeviceIdentity(candidate, resolved.identityKey);
return runOpenClawStateWriteTransaction(({ db }) => {
if (!readStoredIdentityFromDatabase({ db }, resolved.identityKey)) {
assertNoPendingLegacyIdentity({
...options,
path: resolved.databasePath,
identityKey: resolved.identityKey
});
const kysely = getNodeSqliteKysely(db);
executeSqliteQuerySync(db, kysely.insertInto("device_identities").values(storedIdentityToRow(resolved.identityKey, candidate)).onConflict((conflict) => conflict.column("identity_key").doNothing()));
}
const authoritative = readStoredIdentityFromDatabase({ db }, resolved.identityKey);
if (!authoritative) throw new DeviceIdentityStorageError(`SQLite device identity "${resolved.identityKey}" was not durable after insert.`);
return authoritative;
}, {
env: options.env,
path: resolved.databasePath
}, { operationLabel: "device-identity.create" });
}
/** Replace only an invalid authoritative row; preserve a valid concurrent winner. */
function repairInvalidStoredDeviceIdentity(candidate, options = {}) {
const resolved = resolveDeviceIdentityStore(options);
validateStoredDeviceIdentity(candidate, resolved.identityKey);
return runOpenClawStateWriteTransaction(({ db }) => {
let existingRow = null;
try {
existingRow = readStoredIdentityRowFromDatabase({ db }, resolved.identityKey);
const existing = existingRow ? rowToStoredIdentity(existingRow, resolved.identityKey) : null;
if (existing) {
validateStoredDeviceIdentity(existing, resolved.identityKey);
return {
identity: existing,
repaired: false,
rotated: false
};
}
} catch (error) {
if (!(error instanceof DeviceIdentityStorageError)) throw error;
}
const salvaged = existingRow ? salvageStoredIdentityRow(existingRow, resolved.identityKey, candidate.createdAtMs) : null;
const kysely = getNodeSqliteKysely(db);
if (salvaged) executeSqliteQuerySync(db, kysely.updateTable("device_identities").set({
device_id: salvaged.deviceId,
public_key_pem: salvaged.publicKeyPem,
private_key_pem: salvaged.privateKeyPem,
created_at_ms: salvaged.createdAtMs,
updated_at_ms: candidate.createdAtMs
}).where("identity_key", "=", resolved.identityKey));
else {
if (existingRow) executeSqliteQuerySync(db, kysely.deleteFrom("device_identities").where("identity_key", "=", resolved.identityKey));
executeSqliteQuerySync(db, kysely.insertInto("device_identities").values(storedIdentityToRow(resolved.identityKey, candidate)).onConflict((conflict) => conflict.column("identity_key").doNothing()));
}
const authoritative = readStoredIdentityFromDatabase({ db }, resolved.identityKey);
if (!authoritative) throw new DeviceIdentityStorageError(`SQLite device identity "${resolved.identityKey}" was not durable after repair.`);
return {
identity: authoritative,
repaired: true,
rotated: salvaged === null
};
}, {
env: options.env,
path: resolved.databasePath
}, { operationLabel: "device-identity.doctor-repair" });
}
//#endregion
export { readStoredDeviceIdentity as a, resolveDeviceIdentityStore as c, insertStoredDeviceIdentityIfAbsent as i, validateStoredDeviceIdentity as l, assertNoPendingLegacyIdentity as n, readStoredDeviceIdentityReadOnly as o, generateStoredDeviceIdentity as r, repairInvalidStoredDeviceIdentity as s, DeviceIdentityStorageError as t };