UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

252 lines (251 loc) • 12.3 kB
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 };