openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
390 lines (389 loc) • 18.3 kB
JavaScript
import { n as normalizeAccountId } from "./account-id-Df9e41E6.js";
import { r as saveJsonFile, t as loadJsonFile } from "./json-store-CWaMsrLM.js";
import { a as resolveMatrixDefaultOrOnlyAccountId, n as requiresExplicitMatrixDefaultAccount } from "./account-selection-CqvkLWBU.js";
import { t as getMatrixRuntime } from "./runtime-CN4Os2vf.js";
import { n as resolveMatrixAccountStorageRoot, s as resolveMatrixLegacyFlatStoragePaths } from "./storage-paths-BnIxy2QZ.js";
import { b as scoreMatrixCryptoStateInStore, g as readMatrixIdbSnapshotJson, l as migrateLegacyMatrixRecoveryKeyFileToStore, n as MATRIX_LEGACY_CRYPTO_MIGRATION_FILENAME, r as MATRIX_RECOVERY_KEY_FILENAME, s as migrateLegacyMatrixLegacyCryptoMigrationFileToStore, t as MATRIX_IDB_SNAPSHOT_FILENAME, x as writeMatrixIdbSnapshotJson } from "./crypto-state-store-Dh3bqPTW.js";
import fs from "node:fs";
import path from "node:path";
import os from "node:os";
//#region extensions/matrix/src/matrix/client/storage.ts
const DEFAULT_ACCOUNT_KEY = "default";
const STORAGE_META_FILENAME = "storage-meta.json";
const THREAD_BINDINGS_FILENAME = "thread-bindings.json";
function resolveLegacyStoragePaths(env = process.env) {
return resolveMatrixLegacyFlatStoragePaths(getMatrixRuntime().state.resolveStateDir(env, os.homedir));
}
function assertLegacyMigrationAccountSelection(params) {
const cfg = getMatrixRuntime().config.current();
if (!cfg.channels?.matrix || typeof cfg.channels.matrix !== "object") return;
if (requiresExplicitMatrixDefaultAccount(cfg)) throw new Error("Legacy Matrix client storage cannot be migrated automatically because multiple Matrix accounts are configured and channels.matrix.defaultAccount is not set.");
const selectedAccountId = normalizeAccountId(resolveMatrixDefaultOrOnlyAccountId(cfg));
const currentAccountId = normalizeAccountId(params.accountKey);
if (selectedAccountId !== currentAccountId) throw new Error(`Legacy Matrix client storage targets account "${selectedAccountId}", but the current client is starting account "${currentAccountId}". Start the selected account first so flat legacy storage is not migrated into the wrong account directory.`);
}
function scoreStorageRoot(rootDir) {
let score = 0;
if (readStoredRootMetadata(rootDir).currentTokenStateClaimed === true) score += 8;
if (fs.existsSync(path.join(rootDir, "crypto"))) score += 8;
if (fs.existsSync(path.join(rootDir, THREAD_BINDINGS_FILENAME))) score += 4;
if (fs.existsSync(path.join(rootDir, "legacy-crypto-migration.json"))) score += 3;
if (fs.existsSync(path.join(rootDir, "recovery-key.json"))) score += 2;
if (fs.existsSync(path.join(rootDir, "crypto-idb-snapshot.json"))) score += 2;
score += scoreMatrixCryptoStateInStore(rootDir);
if (fs.existsSync(path.join(rootDir, STORAGE_META_FILENAME))) score += 1;
return score;
}
function resolveStorageRootMtimeMs(rootDir) {
try {
return fs.statSync(rootDir).mtimeMs;
} catch {
return 0;
}
}
function readStoredRootMetadata(rootDir) {
const metadata = {};
const parsed = loadJsonFile(path.join(rootDir, STORAGE_META_FILENAME));
if (parsed) {
if (typeof parsed.homeserver === "string" && parsed.homeserver.trim()) metadata.homeserver = parsed.homeserver.trim();
if (typeof parsed.userId === "string" && parsed.userId.trim()) metadata.userId = parsed.userId.trim();
if (typeof parsed.accountId === "string" && parsed.accountId.trim()) metadata.accountId = parsed.accountId.trim();
if (typeof parsed.accessTokenHash === "string" && parsed.accessTokenHash.trim()) metadata.accessTokenHash = parsed.accessTokenHash.trim();
if (typeof parsed.deviceId === "string" && parsed.deviceId.trim()) metadata.deviceId = parsed.deviceId.trim();
if (parsed.currentTokenStateClaimed === true) metadata.currentTokenStateClaimed = true;
if (typeof parsed.createdAt === "string" && parsed.createdAt.trim()) metadata.createdAt = parsed.createdAt.trim();
}
return metadata;
}
function isCompatibleStorageRoot(params) {
const metadata = readStoredRootMetadata(params.candidateRootDir);
if (metadata.homeserver && metadata.homeserver !== params.homeserver) return false;
if (metadata.userId && metadata.userId !== params.userId) return false;
if (metadata.accountId && normalizeAccountId(metadata.accountId) !== normalizeAccountId(params.accountKey)) return false;
if (params.deviceId && metadata.deviceId && metadata.deviceId.trim() && metadata.deviceId.trim() !== params.deviceId.trim()) return false;
if (params.requireExplicitDeviceMatch && params.deviceId && (!metadata.deviceId || metadata.deviceId.trim() !== params.deviceId.trim())) return false;
return true;
}
function resolvePreferredMatrixStorageRoot(params) {
const parentDir = path.dirname(params.canonicalRootDir);
const bestCurrentScore = scoreStorageRoot(params.canonicalRootDir);
let best = {
rootDir: params.canonicalRootDir,
tokenHash: params.canonicalTokenHash,
score: bestCurrentScore,
mtimeMs: resolveStorageRootMtimeMs(params.canonicalRootDir)
};
if (!params.deviceId?.trim()) return {
rootDir: best.rootDir,
tokenHash: best.tokenHash
};
const canonicalMetadata = readStoredRootMetadata(params.canonicalRootDir);
if (canonicalMetadata.accessTokenHash === params.canonicalTokenHash && canonicalMetadata.deviceId?.trim() === params.deviceId.trim() && canonicalMetadata.currentTokenStateClaimed === true) return {
rootDir: best.rootDir,
tokenHash: best.tokenHash
};
let siblingEntries;
try {
siblingEntries = fs.readdirSync(parentDir, { withFileTypes: true });
} catch {
return {
rootDir: best.rootDir,
tokenHash: best.tokenHash
};
}
for (const entry of siblingEntries) {
if (!entry.isDirectory()) continue;
if (entry.name === params.canonicalTokenHash) continue;
const candidateRootDir = path.join(parentDir, entry.name);
if (!isCompatibleStorageRoot({
candidateRootDir,
homeserver: params.homeserver,
userId: params.userId,
accountKey: params.accountKey,
deviceId: params.deviceId,
requireExplicitDeviceMatch: Boolean(params.deviceId)
})) continue;
const candidateScore = scoreStorageRoot(candidateRootDir);
if (candidateScore <= 0) continue;
const candidateMtimeMs = resolveStorageRootMtimeMs(candidateRootDir);
if (candidateScore > best.score || best.rootDir !== params.canonicalRootDir && candidateScore === best.score && candidateMtimeMs > best.mtimeMs) best = {
rootDir: candidateRootDir,
tokenHash: entry.name,
score: candidateScore,
mtimeMs: candidateMtimeMs
};
}
return {
rootDir: best.rootDir,
tokenHash: best.tokenHash
};
}
function resolveMatrixStoragePaths(params) {
const env = params.env ?? process.env;
const canonical = resolveMatrixAccountStorageRoot({
stateDir: params.stateDir ?? getMatrixRuntime().state.resolveStateDir(env, os.homedir),
homeserver: params.homeserver,
userId: params.userId,
accessToken: params.accessToken,
accountId: params.accountId
});
const { rootDir, tokenHash } = resolvePreferredMatrixStorageRoot({
canonicalRootDir: canonical.rootDir,
canonicalTokenHash: canonical.tokenHash,
homeserver: params.homeserver,
userId: params.userId,
accountKey: canonical.accountKey,
deviceId: params.deviceId
});
return {
rootDir,
storagePath: path.join(rootDir, "bot-storage.json"),
cryptoPath: path.join(rootDir, "crypto"),
metaPath: path.join(rootDir, STORAGE_META_FILENAME),
recoveryKeyPath: path.join(rootDir, MATRIX_RECOVERY_KEY_FILENAME),
idbSnapshotPath: path.join(rootDir, MATRIX_IDB_SNAPSHOT_FILENAME),
accountKey: canonical.accountKey,
tokenHash
};
}
function resolveMatrixStateFilePath(params) {
const storagePaths = resolveMatrixStoragePaths({
homeserver: params.auth.homeserver,
userId: params.auth.userId,
accessToken: params.auth.accessToken,
accountId: params.accountId ?? params.auth.accountId,
deviceId: params.auth.deviceId,
env: params.env,
stateDir: params.stateDir
});
return path.join(storagePaths.rootDir, params.filename);
}
async function maybeMigrateLegacyStorage(params) {
const legacy = resolveLegacyStoragePaths(params.env);
const hasFlatLegacyStorageFile = fs.existsSync(legacy.storagePath);
const hasAccountScopedLegacyStorageFile = fs.existsSync(params.storagePaths.storagePath);
const syncCache = hasFlatLegacyStorageFile || hasAccountScopedLegacyStorageFile ? await import("./file-sync-store--TIwkxsz.js") : null;
const hasFlatLegacyStorage = hasFlatLegacyStorageFile && await syncCache?.readLegacyMatrixSyncCacheState(legacy.rootDir) !== null;
const hasAccountScopedLegacyStorage = hasAccountScopedLegacyStorageFile && await syncCache?.readLegacyMatrixSyncCacheState(params.storagePaths.rootDir) !== null;
const hasLegacyCrypto = fs.existsSync(legacy.cryptoPath);
const hasAccountScopedRecoveryKey = fs.existsSync(params.storagePaths.recoveryKeyPath);
const hasAccountScopedIdbSnapshot = fs.existsSync(params.storagePaths.idbSnapshotPath);
const hasAccountScopedLegacyCryptoMigration = fs.existsSync(path.join(params.storagePaths.rootDir, MATRIX_LEGACY_CRYPTO_MIGRATION_FILENAME));
if (!hasFlatLegacyStorage && !hasAccountScopedLegacyStorage && !hasLegacyCrypto && !hasAccountScopedRecoveryKey && !hasAccountScopedIdbSnapshot && !hasAccountScopedLegacyCryptoMigration) return;
const hasTargetCrypto = fs.existsSync(params.storagePaths.cryptoPath);
const shouldMigrateCrypto = hasLegacyCrypto && !hasTargetCrypto;
if (!hasFlatLegacyStorage && !hasAccountScopedLegacyStorage && !shouldMigrateCrypto && !hasAccountScopedRecoveryKey && !hasAccountScopedIdbSnapshot && !hasAccountScopedLegacyCryptoMigration) return;
if (hasFlatLegacyStorage || hasLegacyCrypto) assertLegacyMigrationAccountSelection({ accountKey: params.storagePaths.accountKey });
const logger = getMatrixRuntime().logging.getChildLogger({ module: "matrix-storage" });
const { maybeCreateMatrixMigrationSnapshot } = await import("./migration-snapshot.runtime.js");
await maybeCreateMatrixMigrationSnapshot({
trigger: "matrix-client-fallback",
env: params.env,
log: logger
});
fs.mkdirSync(params.storagePaths.rootDir, { recursive: true });
const moved = [];
const pendingArchives = [];
const skippedExistingTargets = [];
try {
if (hasAccountScopedLegacyStorage) await migrateLegacySyncCacheToSqlite({
sourceRootDir: params.storagePaths.rootDir,
sourcePath: params.storagePaths.storagePath,
targetRootDir: params.storagePaths.rootDir,
label: "account sync cache",
moved,
pendingArchives
});
if (hasFlatLegacyStorage) await migrateLegacySyncCacheToSqlite({
sourceRootDir: legacy.rootDir,
sourcePath: legacy.storagePath,
targetRootDir: params.storagePaths.rootDir,
label: "flat sync cache",
moved,
pendingArchives
});
if (shouldMigrateCrypto) moveLegacyStoragePathOrThrow({
sourcePath: legacy.cryptoPath,
targetPath: params.storagePaths.cryptoPath,
label: "crypto store",
moved
});
else if (hasLegacyCrypto) skippedExistingTargets.push(`- crypto store remains at ${legacy.cryptoPath} because ${params.storagePaths.cryptoPath} already exists`);
if (hasAccountScopedRecoveryKey) {
migrateLegacyMatrixRecoveryKeyFileToStore(params.storagePaths.rootDir);
moved.push({
sourcePath: params.storagePaths.recoveryKeyPath,
targetPath: `${params.storagePaths.rootDir} SQLite recovery key state`,
label: "recovery key"
});
}
if (hasAccountScopedLegacyCryptoMigration) {
migrateLegacyMatrixLegacyCryptoMigrationFileToStore(params.storagePaths.rootDir);
moved.push({
sourcePath: path.join(params.storagePaths.rootDir, MATRIX_LEGACY_CRYPTO_MIGRATION_FILENAME),
targetPath: `${params.storagePaths.rootDir} SQLite legacy crypto migration state`,
label: "legacy crypto migration"
});
}
if (hasAccountScopedIdbSnapshot) await migrateLegacyIdbSnapshotToSqlite({
storageRootDir: params.storagePaths.rootDir,
snapshotPath: params.storagePaths.idbSnapshotPath,
moved,
pendingArchives
});
} catch (err) {
const rollbackError = rollbackLegacyMoves(moved);
throw new Error(rollbackError ? `Failed migrating legacy Matrix client storage: ${String(err)}. Rollback also failed: ${rollbackError}` : `Failed migrating legacy Matrix client storage: ${String(err)}`, { cause: err });
}
for (const archive of pendingArchives) archiveLegacyStoragePath({
...archive,
skippedExistingTargets
});
if (moved.length > 0) logger.info(`matrix: migrated legacy client storage into ${params.storagePaths.rootDir}\n${moved.map((entry) => `- ${entry.label}: ${entry.sourcePath} -> ${entry.targetPath}`).join("\n")}`);
if (skippedExistingTargets.length > 0) logger.warn?.(`matrix: legacy client storage still exists in the flat path because some account-scoped targets already existed.\n${skippedExistingTargets.join("\n")}`);
}
async function migrateLegacyIdbSnapshotToSqlite(params) {
if (readMatrixIdbSnapshotJson(params.storageRootDir)) {
params.pendingArchives.push({
sourcePath: params.snapshotPath,
label: "IndexedDB snapshot"
});
return;
}
const { readLegacyMatrixIdbSnapshotState } = await import("./idb-persistence-C1ok5OI-.js");
const snapshot = await readLegacyMatrixIdbSnapshotState(params.storageRootDir);
if (!snapshot) return;
writeMatrixIdbSnapshotJson({
storageRootDir: params.storageRootDir,
snapshotJson: JSON.stringify(snapshot),
databaseCount: snapshot.length
});
params.moved.push({
sourcePath: params.snapshotPath,
targetPath: `${params.storageRootDir} SQLite IndexedDB snapshot state`,
label: "IndexedDB snapshot"
});
params.pendingArchives.push({
sourcePath: params.snapshotPath,
label: "IndexedDB snapshot"
});
}
async function migrateLegacySyncCacheToSqlite(params) {
const syncCache = await import("./file-sync-store--TIwkxsz.js");
const persisted = await syncCache.readLegacyMatrixSyncCacheState(params.sourceRootDir);
if (!persisted) return;
const store = getMatrixRuntime().state.openKeyedStore(syncCache.openMatrixSyncCacheStoreOptions(params.targetRootDir));
if (!await syncCache.hasMatrixSyncCacheStateInStore({
storageRootDir: params.targetRootDir,
store
})) {
await syncCache.writeMatrixSyncCacheStateToStore({
storageRootDir: params.targetRootDir,
payload: persisted,
store
});
claimCurrentTokenStorageState({ rootDir: params.targetRootDir });
params.moved.push({
sourcePath: params.sourcePath,
targetPath: `${params.targetRootDir} SQLite sync cache`,
label: params.label
});
}
params.pendingArchives.push({
sourcePath: params.sourcePath,
label: params.label
});
}
function archiveLegacyStoragePath(params) {
const archivedLegacyStoragePath = `${params.sourcePath}.migrated`;
if (fs.existsSync(archivedLegacyStoragePath)) {
params.skippedExistingTargets.push(`- ${params.label} remains at ${params.sourcePath} because ${archivedLegacyStoragePath} already exists`);
return;
}
fs.renameSync(params.sourcePath, archivedLegacyStoragePath);
}
function moveLegacyStoragePathOrThrow(params) {
if (!fs.existsSync(params.sourcePath)) return;
if (fs.existsSync(params.targetPath)) throw new Error(`legacy Matrix ${params.label} target already exists (${params.targetPath}); refusing to overwrite it automatically`);
fs.renameSync(params.sourcePath, params.targetPath);
params.moved.push({
sourcePath: params.sourcePath,
targetPath: params.targetPath,
label: params.label
});
}
function rollbackLegacyMoves(moved) {
for (const entry of moved.toReversed()) try {
if (!fs.existsSync(entry.targetPath) || fs.existsSync(entry.sourcePath)) continue;
fs.renameSync(entry.targetPath, entry.sourcePath);
} catch (err) {
return `${entry.label} (${entry.targetPath} -> ${entry.sourcePath}): ${String(err)}`;
}
return null;
}
function writeStoredRootMetadata(metaPath, payload) {
try {
saveJsonFile(metaPath, payload);
return true;
} catch {
return false;
}
}
function writeStorageMeta(params) {
const existing = readStoredRootMetadata(params.storagePaths.rootDir);
return writeStoredRootMetadata(params.storagePaths.metaPath, {
homeserver: params.homeserver,
userId: params.userId,
accountId: params.accountId ?? DEFAULT_ACCOUNT_KEY,
accessTokenHash: params.storagePaths.tokenHash,
deviceId: params.deviceId ?? null,
currentTokenStateClaimed: params.currentTokenStateClaimed ?? existing.currentTokenStateClaimed === true,
createdAt: existing.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()
});
}
function claimCurrentTokenStorageState(params) {
const metadata = readStoredRootMetadata(params.rootDir);
if (!metadata.accessTokenHash?.trim()) return false;
return writeStoredRootMetadata(path.join(params.rootDir, STORAGE_META_FILENAME), {
homeserver: metadata.homeserver,
userId: metadata.userId,
accountId: metadata.accountId ?? DEFAULT_ACCOUNT_KEY,
accessTokenHash: metadata.accessTokenHash,
deviceId: metadata.deviceId ?? null,
currentTokenStateClaimed: true,
createdAt: metadata.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()
});
}
function recordCurrentStorageMetaDeviceId(params) {
const deviceId = params.deviceId.trim();
if (!deviceId) return false;
const metadata = readStoredRootMetadata(params.rootDir);
if (!metadata.accessTokenHash?.trim()) return false;
return writeStoredRootMetadata(path.join(params.rootDir, STORAGE_META_FILENAME), {
homeserver: metadata.homeserver,
userId: metadata.userId,
accountId: metadata.accountId ?? DEFAULT_ACCOUNT_KEY,
accessTokenHash: metadata.accessTokenHash,
deviceId,
currentTokenStateClaimed: metadata.currentTokenStateClaimed === true,
createdAt: metadata.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()
});
}
function repairCurrentTokenStorageMetaDeviceId(params) {
return writeStorageMeta({
storagePaths: resolveMatrixStoragePaths({
homeserver: params.homeserver,
userId: params.userId,
accessToken: params.accessToken,
accountId: params.accountId,
env: params.env,
stateDir: params.stateDir
}),
homeserver: params.homeserver,
userId: params.userId,
accountId: params.accountId,
deviceId: params.deviceId
});
}
//#endregion
export { resolveMatrixStateFilePath as a, repairCurrentTokenStorageMetaDeviceId as i, maybeMigrateLegacyStorage as n, resolveMatrixStoragePaths as o, recordCurrentStorageMetaDeviceId as r, writeStorageMeta as s, claimCurrentTokenStorageState as t };