mirror of https://github.com/openclaw/openclaw.git
Matrix: keep fresh devices out of stale storage roots
This commit is contained in:
parent
c1f4cbf6e7
commit
d58e51e46b
|
|
@ -31,6 +31,7 @@ export async function createMatrixClient(params: {
|
|||
userId,
|
||||
accessToken: params.accessToken,
|
||||
accountId: params.accountId,
|
||||
deviceId: params.deviceId,
|
||||
env,
|
||||
});
|
||||
await maybeMigrateLegacyStorage({
|
||||
|
|
@ -44,6 +45,7 @@ export async function createMatrixClient(params: {
|
|||
homeserver,
|
||||
userId,
|
||||
accountId: params.accountId,
|
||||
deviceId: params.deviceId,
|
||||
});
|
||||
|
||||
const cryptoDatabasePrefix = `openclaw-matrix-${storagePaths.accountKey}-${storagePaths.tokenHash}`;
|
||||
|
|
|
|||
|
|
@ -316,4 +316,54 @@ describe("matrix client storage paths", () => {
|
|||
expect(resolvedPaths.rootDir).toBe(oldStoragePaths.rootDir);
|
||||
expect(resolvedPaths.tokenHash).toBe(oldStoragePaths.tokenHash);
|
||||
});
|
||||
|
||||
it("does not reuse a populated sibling storage root from a different device", () => {
|
||||
const stateDir = setupStateDir();
|
||||
const oldStoragePaths = resolveMatrixStoragePaths({
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@bot:example.org",
|
||||
accessToken: "secret-token-old",
|
||||
deviceId: "OLDDEVICE",
|
||||
env: {},
|
||||
});
|
||||
fs.mkdirSync(oldStoragePaths.rootDir, { recursive: true });
|
||||
fs.writeFileSync(oldStoragePaths.storagePath, '{"legacy":true}');
|
||||
fs.writeFileSync(
|
||||
path.join(oldStoragePaths.rootDir, "startup-verification.json"),
|
||||
JSON.stringify({ deviceId: "OLDDEVICE" }, null, 2),
|
||||
);
|
||||
|
||||
const newerCanonicalPaths = resolveMatrixAccountStorageRoot({
|
||||
stateDir,
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@bot:example.org",
|
||||
accessToken: "secret-token-new",
|
||||
});
|
||||
fs.mkdirSync(newerCanonicalPaths.rootDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(newerCanonicalPaths.rootDir, "storage-meta.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@bot:example.org",
|
||||
accountId: "default",
|
||||
accessTokenHash: newerCanonicalPaths.tokenHash,
|
||||
deviceId: "NEWDEVICE",
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
|
||||
const resolvedPaths = resolveMatrixStoragePaths({
|
||||
homeserver: "https://matrix.example.org",
|
||||
userId: "@bot:example.org",
|
||||
accessToken: "secret-token-new",
|
||||
deviceId: "NEWDEVICE",
|
||||
env: {},
|
||||
});
|
||||
|
||||
expect(resolvedPaths.rootDir).toBe(newerCanonicalPaths.rootDir);
|
||||
expect(resolvedPaths.tokenHash).toBe(newerCanonicalPaths.tokenHash);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ const THREAD_BINDINGS_FILENAME = "thread-bindings.json";
|
|||
const LEGACY_CRYPTO_MIGRATION_FILENAME = "legacy-crypto-migration.json";
|
||||
const RECOVERY_KEY_FILENAME = "recovery-key.json";
|
||||
const IDB_SNAPSHOT_FILENAME = "crypto-idb-snapshot.json";
|
||||
const STARTUP_VERIFICATION_FILENAME = "startup-verification.json";
|
||||
|
||||
type LegacyMoveRecord = {
|
||||
sourcePath: string;
|
||||
|
|
@ -25,6 +26,14 @@ type LegacyMoveRecord = {
|
|||
label: string;
|
||||
};
|
||||
|
||||
type StoredRootMetadata = {
|
||||
homeserver?: string;
|
||||
userId?: string;
|
||||
accountId?: string;
|
||||
accessTokenHash?: string;
|
||||
deviceId?: string | null;
|
||||
};
|
||||
|
||||
function resolveLegacyStoragePaths(env: NodeJS.ProcessEnv = process.env): {
|
||||
storagePath: string;
|
||||
cryptoPath: string;
|
||||
|
|
@ -88,9 +97,84 @@ function resolveStorageRootMtimeMs(rootDir: string): number {
|
|||
}
|
||||
}
|
||||
|
||||
function readStoredRootMetadata(rootDir: string): StoredRootMetadata {
|
||||
const metadata: StoredRootMetadata = {};
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(
|
||||
fs.readFileSync(path.join(rootDir, STORAGE_META_FILENAME), "utf8"),
|
||||
) as Partial<StoredRootMetadata>;
|
||||
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();
|
||||
}
|
||||
} catch {
|
||||
// ignore missing or malformed storage metadata
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(
|
||||
fs.readFileSync(path.join(rootDir, STARTUP_VERIFICATION_FILENAME), "utf8"),
|
||||
) as { deviceId?: unknown };
|
||||
if (!metadata.deviceId && typeof parsed.deviceId === "string" && parsed.deviceId.trim()) {
|
||||
metadata.deviceId = parsed.deviceId.trim();
|
||||
}
|
||||
} catch {
|
||||
// ignore missing or malformed verification state
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
function isCompatibleStorageRoot(params: {
|
||||
candidateRootDir: string;
|
||||
homeserver: string;
|
||||
userId: string;
|
||||
accountKey: string;
|
||||
deviceId?: string | null;
|
||||
}): boolean {
|
||||
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;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function resolvePreferredMatrixStorageRoot(params: {
|
||||
canonicalRootDir: string;
|
||||
canonicalTokenHash: string;
|
||||
homeserver: string;
|
||||
userId: string;
|
||||
accountKey: string;
|
||||
deviceId?: string | null;
|
||||
}): {
|
||||
rootDir: string;
|
||||
tokenHash: string;
|
||||
|
|
@ -122,6 +206,17 @@ function resolvePreferredMatrixStorageRoot(params: {
|
|||
continue;
|
||||
}
|
||||
const candidateRootDir = path.join(parentDir, entry.name);
|
||||
if (
|
||||
!isCompatibleStorageRoot({
|
||||
candidateRootDir,
|
||||
homeserver: params.homeserver,
|
||||
userId: params.userId,
|
||||
accountKey: params.accountKey,
|
||||
deviceId: params.deviceId,
|
||||
})
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const candidateScore = scoreStorageRoot(candidateRootDir);
|
||||
if (candidateScore <= 0) {
|
||||
continue;
|
||||
|
|
@ -153,6 +248,7 @@ export function resolveMatrixStoragePaths(params: {
|
|||
userId: string;
|
||||
accessToken: string;
|
||||
accountId?: string | null;
|
||||
deviceId?: string | null;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
stateDir?: string;
|
||||
}): MatrixStoragePaths {
|
||||
|
|
@ -168,6 +264,10 @@ export function resolveMatrixStoragePaths(params: {
|
|||
const { rootDir, tokenHash } = resolvePreferredMatrixStorageRoot({
|
||||
canonicalRootDir: canonical.rootDir,
|
||||
canonicalTokenHash: canonical.tokenHash,
|
||||
homeserver: params.homeserver,
|
||||
userId: params.userId,
|
||||
accountKey: canonical.accountKey,
|
||||
deviceId: params.deviceId,
|
||||
});
|
||||
return {
|
||||
rootDir,
|
||||
|
|
@ -301,6 +401,7 @@ export function writeStorageMeta(params: {
|
|||
homeserver: string;
|
||||
userId: string;
|
||||
accountId?: string | null;
|
||||
deviceId?: string | null;
|
||||
}): void {
|
||||
try {
|
||||
const payload = {
|
||||
|
|
@ -308,6 +409,7 @@ export function writeStorageMeta(params: {
|
|||
userId: params.userId,
|
||||
accountId: params.accountId ?? DEFAULT_ACCOUNT_KEY,
|
||||
accessTokenHash: params.storagePaths.tokenHash,
|
||||
deviceId: params.deviceId ?? null,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
fs.mkdirSync(params.storagePaths.rootDir, { recursive: true });
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ function isMigrationState(value: unknown): value is MatrixLegacyCryptoMigrationS
|
|||
|
||||
async function resolvePendingMigrationStatePath(params: {
|
||||
stateDir: string;
|
||||
auth: Pick<MatrixAuth, "homeserver" | "userId" | "accessToken" | "accountId">;
|
||||
auth: Pick<MatrixAuth, "homeserver" | "userId" | "accessToken" | "accountId" | "deviceId">;
|
||||
}): Promise<{
|
||||
statePath: string;
|
||||
value: MatrixLegacyCryptoMigrationState | null;
|
||||
|
|
@ -53,6 +53,7 @@ async function resolvePendingMigrationStatePath(params: {
|
|||
userId: params.auth.userId,
|
||||
accessToken: params.auth.accessToken,
|
||||
accountId: params.auth.accountId,
|
||||
deviceId: params.auth.deviceId,
|
||||
stateDir: params.stateDir,
|
||||
});
|
||||
const directStatePath = path.join(rootDir, "legacy-crypto-migration.json");
|
||||
|
|
@ -89,7 +90,7 @@ async function resolvePendingMigrationStatePath(params: {
|
|||
|
||||
export async function maybeRestoreLegacyMatrixBackup(params: {
|
||||
client: Pick<MatrixClient, "restoreRoomKeyBackup">;
|
||||
auth: Pick<MatrixAuth, "homeserver" | "userId" | "accessToken" | "accountId">;
|
||||
auth: Pick<MatrixAuth, "homeserver" | "userId" | "accessToken" | "accountId" | "deviceId">;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
stateDir?: string;
|
||||
}): Promise<MatrixLegacyCryptoRestoreResult> {
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ function resolveStartupVerificationStatePath(params: {
|
|||
userId: params.auth.userId,
|
||||
accessToken: params.auth.accessToken,
|
||||
accountId: params.auth.accountId,
|
||||
deviceId: params.auth.deviceId,
|
||||
env: params.env,
|
||||
});
|
||||
return path.join(storagePaths.rootDir, STARTUP_VERIFICATION_STATE_FILENAME);
|
||||
|
|
|
|||
|
|
@ -179,6 +179,7 @@ function resolveBindingsPath(params: {
|
|||
userId: params.auth.userId,
|
||||
accessToken: params.auth.accessToken,
|
||||
accountId: params.accountId,
|
||||
deviceId: params.auth.deviceId,
|
||||
env: params.env,
|
||||
});
|
||||
return path.join(storagePaths.rootDir, "thread-bindings.json");
|
||||
|
|
|
|||
Loading…
Reference in New Issue