diff --git a/extensions/matrix/src/cli.test.ts b/extensions/matrix/src/cli.test.ts index 3be18e47a90..43468374c3a 100644 --- a/extensions/matrix/src/cli.test.ts +++ b/extensions/matrix/src/cli.test.ts @@ -5,6 +5,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const bootstrapMatrixVerificationMock = vi.fn(); const getMatrixRoomKeyBackupStatusMock = vi.fn(); const getMatrixVerificationStatusMock = vi.fn(); +const listMatrixOwnDevicesMock = vi.fn(); +const pruneMatrixStaleGatewayDevicesMock = vi.fn(); const resolveMatrixAccountConfigMock = vi.fn(); const resolveMatrixAccountMock = vi.fn(); const matrixSetupApplyAccountConfigMock = vi.fn(); @@ -25,6 +27,12 @@ vi.mock("./matrix/actions/verification.js", () => ({ verifyMatrixRecoveryKey: (...args: unknown[]) => verifyMatrixRecoveryKeyMock(...args), })); +vi.mock("./matrix/actions/devices.js", () => ({ + listMatrixOwnDevices: (...args: unknown[]) => listMatrixOwnDevicesMock(...args), + pruneMatrixStaleGatewayDevices: (...args: unknown[]) => + pruneMatrixStaleGatewayDevicesMock(...args), +})); + vi.mock("./matrix/client/logging.js", () => ({ setMatrixSdkConsoleLogging: (...args: unknown[]) => setMatrixSdkConsoleLoggingMock(...args), setMatrixSdkLogMode: (...args: unknown[]) => setMatrixSdkLogModeMock(...args), @@ -104,6 +112,14 @@ describe("matrix CLI verification commands", () => { resolvedAvatarUrl: null, convertedAvatarFromHttp: false, }); + listMatrixOwnDevicesMock.mockResolvedValue([]); + pruneMatrixStaleGatewayDevicesMock.mockResolvedValue({ + before: [], + staleGatewayDeviceIds: [], + currentDeviceId: null, + deletedDeviceIds: [], + remainingDevices: [], + }); }); afterEach(() => { @@ -166,6 +182,77 @@ describe("matrix CLI verification commands", () => { expect(process.exitCode).toBe(1); }); + it("lists matrix devices", async () => { + listMatrixOwnDevicesMock.mockResolvedValue([ + { + deviceId: "A7hWrQ70ea", + displayName: "OpenClaw Gateway", + lastSeenIp: "127.0.0.1", + lastSeenTs: 1_741_507_200_000, + current: true, + }, + { + deviceId: "BritdXC6iL", + displayName: "OpenClaw Gateway", + lastSeenIp: null, + lastSeenTs: null, + current: false, + }, + ]); + const program = buildProgram(); + + await program.parseAsync(["matrix", "devices", "list", "--account", "poe"], { from: "user" }); + + expect(listMatrixOwnDevicesMock).toHaveBeenCalledWith({ accountId: "poe" }); + expect(console.log).toHaveBeenCalledWith("Account: poe"); + expect(console.log).toHaveBeenCalledWith("- A7hWrQ70ea (current, OpenClaw Gateway)"); + expect(console.log).toHaveBeenCalledWith(" Last IP: 127.0.0.1"); + expect(console.log).toHaveBeenCalledWith("- BritdXC6iL (OpenClaw Gateway)"); + }); + + it("prunes stale matrix gateway devices", async () => { + pruneMatrixStaleGatewayDevicesMock.mockResolvedValue({ + before: [ + { + deviceId: "A7hWrQ70ea", + displayName: "OpenClaw Gateway", + lastSeenIp: "127.0.0.1", + lastSeenTs: 1_741_507_200_000, + current: true, + }, + { + deviceId: "BritdXC6iL", + displayName: "OpenClaw Gateway", + lastSeenIp: null, + lastSeenTs: null, + current: false, + }, + ], + staleGatewayDeviceIds: ["BritdXC6iL"], + currentDeviceId: "A7hWrQ70ea", + deletedDeviceIds: ["BritdXC6iL"], + remainingDevices: [ + { + deviceId: "A7hWrQ70ea", + displayName: "OpenClaw Gateway", + lastSeenIp: "127.0.0.1", + lastSeenTs: 1_741_507_200_000, + current: true, + }, + ], + }); + const program = buildProgram(); + + await program.parseAsync(["matrix", "devices", "prune-stale", "--account", "poe"], { + from: "user", + }); + + expect(pruneMatrixStaleGatewayDevicesMock).toHaveBeenCalledWith({ accountId: "poe" }); + expect(console.log).toHaveBeenCalledWith("Deleted stale OpenClaw devices: BritdXC6iL"); + expect(console.log).toHaveBeenCalledWith("Current device: A7hWrQ70ea"); + expect(console.log).toHaveBeenCalledWith("Remaining devices: 1"); + }); + it("adds a matrix account and prints a binding hint", async () => { matrixRuntimeLoadConfigMock.mockReturnValue({ channels: {} }); matrixSetupApplyAccountConfigMock.mockImplementation( @@ -235,6 +322,22 @@ describe("matrix CLI verification commands", () => { resolveMatrixAccountConfigMock.mockReturnValue({ encryption: true, }); + listMatrixOwnDevicesMock.mockResolvedValue([ + { + deviceId: "BritdXC6iL", + displayName: "OpenClaw Gateway", + lastSeenIp: null, + lastSeenTs: null, + current: false, + }, + { + deviceId: "du314Zpw3A", + displayName: "OpenClaw Gateway", + lastSeenIp: null, + lastSeenTs: null, + current: true, + }, + ]); bootstrapMatrixVerificationMock.mockResolvedValue({ success: true, verification: { @@ -270,6 +373,9 @@ describe("matrix CLI verification commands", () => { `Recovery key created at: ${formatExpectedLocalTimestamp("2026-03-09T06:00:00.000Z")}`, ); expect(console.log).toHaveBeenCalledWith("Backup version: 7"); + expect(console.log).toHaveBeenCalledWith( + "Matrix device hygiene warning: stale OpenClaw devices detected (BritdXC6iL). Run 'openclaw matrix devices prune-stale --account ops'.", + ); }); it("does not bootstrap verification when updating an already configured account", async () => { diff --git a/extensions/matrix/src/cli.ts b/extensions/matrix/src/cli.ts index 163dab0afd3..e8c56178f8e 100644 --- a/extensions/matrix/src/cli.ts +++ b/extensions/matrix/src/cli.ts @@ -6,6 +6,7 @@ import { } from "openclaw/plugin-sdk/matrix"; import { matrixPlugin } from "./channel.js"; import { resolveMatrixAccount, resolveMatrixAccountConfig } from "./matrix/accounts.js"; +import { listMatrixOwnDevices, pruneMatrixStaleGatewayDevices } from "./matrix/actions/devices.js"; import { updateMatrixOwnProfile } from "./matrix/actions/profile.js"; import { bootstrapMatrixVerification, @@ -16,6 +17,7 @@ import { } from "./matrix/actions/verification.js"; import { setMatrixSdkConsoleLogging, setMatrixSdkLogMode } from "./matrix/client/logging.js"; import { resolveMatrixConfigPath, updateMatrixAccountConfig } from "./matrix/config-update.js"; +import { isOpenClawManagedMatrixDevice } from "./matrix/device-health.js"; import { applyMatrixProfileUpdate, type MatrixProfileUpdateResult } from "./profile-update.js"; import { getMatrixRuntime } from "./runtime.js"; import type { CoreConfig } from "./types.js"; @@ -67,6 +69,31 @@ function printAccountLabel(accountId?: string): void { console.log(`Account: ${normalizeAccountId(accountId)}`); } +function printMatrixOwnDevices( + devices: Array<{ + deviceId: string; + displayName: string | null; + lastSeenIp: string | null; + lastSeenTs: number | null; + current: boolean; + }>, +): void { + if (devices.length === 0) { + console.log("Devices: none"); + return; + } + for (const device of devices) { + const labels = [device.current ? "current" : null, device.displayName].filter(Boolean); + console.log(`- ${device.deviceId}${labels.length ? ` (${labels.join(", ")})` : ""}`); + if (device.lastSeenTs) { + printTimestamp(" Last seen", new Date(device.lastSeenTs).toISOString()); + } + if (device.lastSeenIp) { + console.log(` Last IP: ${device.lastSeenIp}`); + } + } +} + function configureCliLogMode(verbose: boolean): void { setMatrixSdkLogMode(verbose ? "default" : "quiet"); setMatrixSdkConsoleLogging(verbose); @@ -88,6 +115,10 @@ type MatrixCliAccountAddResult = { accountId: string; configPath: string; useEnv: boolean; + deviceHealth: { + currentDeviceId: string | null; + staleOpenClawDeviceIds: string[]; + }; verificationBootstrap: { attempted: boolean; success: boolean; @@ -233,10 +264,20 @@ async function addMatrixAccount(params: { } } + const addedDevices = await listMatrixOwnDevices({ accountId }); + const currentDeviceId = addedDevices.find((device) => device.current)?.deviceId ?? null; + const staleOpenClawDeviceIds = addedDevices + .filter((device) => !device.current && isOpenClawManagedMatrixDevice(device.displayName)) + .map((device) => device.deviceId); + return { accountId, configPath: resolveMatrixConfigPath(updated, accountId), useEnv: input.useEnv === true, + deviceHealth: { + currentDeviceId, + staleOpenClawDeviceIds, + }, verificationBootstrap, profile, }; @@ -638,6 +679,11 @@ export function registerMatrixCli(params: { program: Command }): void { ); } } + if (result.deviceHealth.staleOpenClawDeviceIds.length > 0) { + console.log( + `Matrix device hygiene warning: stale OpenClaw devices detected (${result.deviceHealth.staleOpenClawDeviceIds.join(", ")}). Run 'openclaw matrix devices prune-stale --account ${result.accountId}'.`, + ); + } if (result.profile.attempted) { if (result.profile.error) { console.error(`Profile sync warning: ${result.profile.error}`); @@ -900,4 +946,54 @@ export function registerMatrixCli(params: { program: Command }): void { }); }, ); + + const devices = root.command("devices").description("Inspect and clean up Matrix devices"); + + devices + .command("list") + .description("List server-side Matrix devices for this account") + .option("--account ", "Account ID (for multi-account setups)") + .option("--verbose", "Show detailed diagnostics") + .option("--json", "Output as JSON") + .action(async (options: { account?: string; verbose?: boolean; json?: boolean }) => { + await runMatrixCliCommand({ + verbose: options.verbose === true, + json: options.json === true, + run: async () => await listMatrixOwnDevices({ accountId: options.account }), + onText: (result) => { + printAccountLabel(options.account); + printMatrixOwnDevices(result); + }, + errorPrefix: "Device listing failed", + }); + }); + + devices + .command("prune-stale") + .description("Delete stale OpenClaw-managed devices for this account") + .option("--account ", "Account ID (for multi-account setups)") + .option("--verbose", "Show detailed diagnostics") + .option("--json", "Output as JSON") + .action(async (options: { account?: string; verbose?: boolean; json?: boolean }) => { + await runMatrixCliCommand({ + verbose: options.verbose === true, + json: options.json === true, + run: async () => await pruneMatrixStaleGatewayDevices({ accountId: options.account }), + onText: (result, verbose) => { + printAccountLabel(options.account); + console.log( + `Deleted stale OpenClaw devices: ${result.deletedDeviceIds.length ? result.deletedDeviceIds.join(", ") : "none"}`, + ); + console.log(`Current device: ${result.currentDeviceId ?? "unknown"}`); + console.log(`Remaining devices: ${result.remainingDevices.length}`); + if (verbose) { + console.log("Devices before cleanup:"); + printMatrixOwnDevices(result.before); + console.log("Devices after cleanup:"); + printMatrixOwnDevices(result.remainingDevices); + } + }, + errorPrefix: "Device cleanup failed", + }); + }); } diff --git a/extensions/matrix/src/matrix/actions/devices.test.ts b/extensions/matrix/src/matrix/actions/devices.test.ts new file mode 100644 index 00000000000..4a90d5e1925 --- /dev/null +++ b/extensions/matrix/src/matrix/actions/devices.test.ts @@ -0,0 +1,118 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const withResolvedActionClientMock = vi.fn(); + +vi.mock("./client.js", () => ({ + withResolvedActionClient: (...args: unknown[]) => withResolvedActionClientMock(...args), +})); + +let listMatrixOwnDevices: typeof import("./devices.js").listMatrixOwnDevices; +let pruneMatrixStaleGatewayDevices: typeof import("./devices.js").pruneMatrixStaleGatewayDevices; + +describe("matrix device actions", () => { + beforeEach(async () => { + vi.resetModules(); + vi.clearAllMocks(); + ({ listMatrixOwnDevices, pruneMatrixStaleGatewayDevices } = await import("./devices.js")); + }); + + it("lists own devices on a started client", async () => { + withResolvedActionClientMock.mockImplementation(async (_opts, run) => { + return await run({ + listOwnDevices: vi.fn(async () => [ + { + deviceId: "A7hWrQ70ea", + displayName: "OpenClaw Gateway", + lastSeenIp: null, + lastSeenTs: null, + current: true, + }, + ]), + }); + }); + + const result = await listMatrixOwnDevices({ accountId: "poe" }); + + expect(withResolvedActionClientMock).toHaveBeenCalledWith( + { accountId: "poe", readiness: "started" }, + expect.any(Function), + "persist", + ); + expect(result).toEqual([ + expect.objectContaining({ + deviceId: "A7hWrQ70ea", + current: true, + }), + ]); + }); + + it("prunes stale OpenClaw-managed devices but preserves the current device", async () => { + const deleteOwnDevices = vi.fn(async () => ({ + currentDeviceId: "du314Zpw3A", + deletedDeviceIds: ["BritdXC6iL", "G6NJU9cTgs", "My3T0hkTE0"], + remainingDevices: [ + { + deviceId: "du314Zpw3A", + displayName: "OpenClaw Gateway", + lastSeenIp: null, + lastSeenTs: null, + current: true, + }, + ], + })); + withResolvedActionClientMock.mockImplementation(async (_opts, run) => { + return await run({ + listOwnDevices: vi.fn(async () => [ + { + deviceId: "du314Zpw3A", + displayName: "OpenClaw Gateway", + lastSeenIp: null, + lastSeenTs: null, + current: true, + }, + { + deviceId: "BritdXC6iL", + displayName: "OpenClaw Gateway", + lastSeenIp: null, + lastSeenTs: null, + current: false, + }, + { + deviceId: "G6NJU9cTgs", + displayName: "OpenClaw Debug", + lastSeenIp: null, + lastSeenTs: null, + current: false, + }, + { + deviceId: "My3T0hkTE0", + displayName: "OpenClaw Gateway", + lastSeenIp: null, + lastSeenTs: null, + current: false, + }, + { + deviceId: "phone123", + displayName: "Element iPhone", + lastSeenIp: null, + lastSeenTs: null, + current: false, + }, + ]), + deleteOwnDevices, + }); + }); + + const result = await pruneMatrixStaleGatewayDevices({ accountId: "poe" }); + + expect(deleteOwnDevices).toHaveBeenCalledWith(["BritdXC6iL", "G6NJU9cTgs", "My3T0hkTE0"]); + expect(result.staleGatewayDeviceIds).toEqual(["BritdXC6iL", "G6NJU9cTgs", "My3T0hkTE0"]); + expect(result.deletedDeviceIds).toEqual(["BritdXC6iL", "G6NJU9cTgs", "My3T0hkTE0"]); + expect(result.remainingDevices).toEqual([ + expect.objectContaining({ + deviceId: "du314Zpw3A", + current: true, + }), + ]); + }); +}); diff --git a/extensions/matrix/src/matrix/actions/devices.ts b/extensions/matrix/src/matrix/actions/devices.ts new file mode 100644 index 00000000000..452f4e72d7e --- /dev/null +++ b/extensions/matrix/src/matrix/actions/devices.ts @@ -0,0 +1,44 @@ +import { summarizeMatrixDeviceHealth } from "../device-health.js"; +import { withResolvedActionClient } from "./client.js"; +import type { MatrixActionClientOpts } from "./types.js"; + +export async function listMatrixOwnDevices(opts: MatrixActionClientOpts = {}) { + return await withResolvedActionClient( + { ...opts, readiness: "started" }, + async (client) => await client.listOwnDevices(), + "persist", + ); +} + +export async function pruneMatrixStaleGatewayDevices(opts: MatrixActionClientOpts = {}) { + return await withResolvedActionClient( + { ...opts, readiness: "started" }, + async (client) => { + const devices = await client.listOwnDevices(); + const health = summarizeMatrixDeviceHealth(devices); + const staleGatewayDeviceIds = health.staleOpenClawDevices.map((device) => device.deviceId); + const deleted = + staleGatewayDeviceIds.length > 0 + ? await client.deleteOwnDevices(staleGatewayDeviceIds) + : { + currentDeviceId: devices.find((device) => device.current)?.deviceId ?? null, + deletedDeviceIds: [] as string[], + remainingDevices: devices, + }; + return { + before: devices, + staleGatewayDeviceIds, + ...deleted, + }; + }, + "persist", + ); +} + +export async function getMatrixDeviceHealth(opts: MatrixActionClientOpts = {}) { + return await withResolvedActionClient( + { ...opts, readiness: "started" }, + async (client) => summarizeMatrixDeviceHealth(await client.listOwnDevices()), + "persist", + ); +} diff --git a/extensions/matrix/src/matrix/device-health.test.ts b/extensions/matrix/src/matrix/device-health.test.ts new file mode 100644 index 00000000000..8de5d825251 --- /dev/null +++ b/extensions/matrix/src/matrix/device-health.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from "vitest"; +import { isOpenClawManagedMatrixDevice, summarizeMatrixDeviceHealth } from "./device-health.js"; + +describe("matrix device health", () => { + it("detects OpenClaw-managed device names", () => { + expect(isOpenClawManagedMatrixDevice("OpenClaw Gateway")).toBe(true); + expect(isOpenClawManagedMatrixDevice("OpenClaw Debug")).toBe(true); + expect(isOpenClawManagedMatrixDevice("Element iPhone")).toBe(false); + expect(isOpenClawManagedMatrixDevice(null)).toBe(false); + }); + + it("summarizes stale OpenClaw-managed devices separately from the current device", () => { + const summary = summarizeMatrixDeviceHealth([ + { + deviceId: "du314Zpw3A", + displayName: "OpenClaw Gateway", + current: true, + }, + { + deviceId: "BritdXC6iL", + displayName: "OpenClaw Gateway", + current: false, + }, + { + deviceId: "G6NJU9cTgs", + displayName: "OpenClaw Debug", + current: false, + }, + { + deviceId: "phone123", + displayName: "Element iPhone", + current: false, + }, + ]); + + expect(summary.currentDeviceId).toBe("du314Zpw3A"); + expect(summary.currentOpenClawDevices).toEqual([ + expect.objectContaining({ deviceId: "du314Zpw3A" }), + ]); + expect(summary.staleOpenClawDevices).toEqual([ + expect.objectContaining({ deviceId: "BritdXC6iL" }), + expect.objectContaining({ deviceId: "G6NJU9cTgs" }), + ]); + }); +}); diff --git a/extensions/matrix/src/matrix/device-health.ts b/extensions/matrix/src/matrix/device-health.ts new file mode 100644 index 00000000000..6f0d4408a55 --- /dev/null +++ b/extensions/matrix/src/matrix/device-health.ts @@ -0,0 +1,31 @@ +export type MatrixManagedDeviceInfo = { + deviceId: string; + displayName: string | null; + current: boolean; +}; + +export type MatrixDeviceHealthSummary = { + currentDeviceId: string | null; + staleOpenClawDevices: MatrixManagedDeviceInfo[]; + currentOpenClawDevices: MatrixManagedDeviceInfo[]; +}; + +const OPENCLAW_DEVICE_NAME_PREFIX = "OpenClaw "; + +export function isOpenClawManagedMatrixDevice(displayName: string | null | undefined): boolean { + return displayName?.startsWith(OPENCLAW_DEVICE_NAME_PREFIX) === true; +} + +export function summarizeMatrixDeviceHealth( + devices: MatrixManagedDeviceInfo[], +): MatrixDeviceHealthSummary { + const currentDeviceId = devices.find((device) => device.current)?.deviceId ?? null; + const openClawDevices = devices.filter((device) => + isOpenClawManagedMatrixDevice(device.displayName), + ); + return { + currentDeviceId, + staleOpenClawDevices: openClawDevices.filter((device) => !device.current), + currentOpenClawDevices: openClawDevices.filter((device) => device.current), + }; +} diff --git a/extensions/matrix/src/matrix/monitor/index.ts b/extensions/matrix/src/matrix/monitor/index.ts index 17f30a775eb..c8d148e28fb 100644 --- a/extensions/matrix/src/matrix/monitor/index.ts +++ b/extensions/matrix/src/matrix/monitor/index.ts @@ -23,6 +23,7 @@ import { stopSharedClientForAccount, } from "../client.js"; import { updateMatrixAccountConfig } from "../config-update.js"; +import { summarizeMatrixDeviceHealth } from "../device-health.js"; import { syncMatrixOwnProfile } from "../profile.js"; import { createMatrixThreadBindingManager } from "../thread-bindings.js"; import { normalizeMatrixUserId } from "./allowlist.js"; @@ -400,6 +401,19 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi // If E2EE is enabled, report device verification status and request self-verification // when configured and the device is still unverified. if (auth.encryption && client.crypto) { + try { + const deviceHealth = summarizeMatrixDeviceHealth(await client.listOwnDevices()); + if (deviceHealth.staleOpenClawDevices.length > 0) { + logger.warn( + `matrix: stale OpenClaw devices detected for ${auth.userId}: ${deviceHealth.staleOpenClawDevices.map((device) => device.deviceId).join(", ")}. Run 'openclaw matrix devices prune-stale --account ${effectiveAccountId}' to keep encrypted-room trust healthy.`, + ); + } + } catch (err) { + logger.debug?.("Failed to inspect matrix device hygiene (non-fatal)", { + error: String(err), + }); + } + try { const startupVerification = await ensureMatrixStartupVerification({ client, diff --git a/extensions/matrix/src/matrix/sdk.ts b/extensions/matrix/src/matrix/sdk.ts index d6ac4f3de9a..925811354fc 100644 --- a/extensions/matrix/src/matrix/sdk.ts +++ b/extensions/matrix/src/matrix/sdk.ts @@ -107,6 +107,20 @@ export type MatrixVerificationBootstrapResult = { cryptoBootstrap: MatrixCryptoBootstrapResult | null; }; +export type MatrixOwnDeviceInfo = { + deviceId: string; + displayName: string | null; + lastSeenIp: string | null; + lastSeenTs: number | null; + current: boolean; +}; + +export type MatrixOwnDeviceDeleteResult = { + currentDeviceId: string | null; + deletedDeviceIds: string[]; + remainingDevices: MatrixOwnDeviceInfo[]; +}; + function normalizeOptionalString(value: string | null | undefined): string | null { const normalized = value?.trim(); return normalized ? normalized : null; @@ -983,6 +997,68 @@ export class MatrixClient { }; } + async listOwnDevices(): Promise { + const currentDeviceId = this.client.getDeviceId()?.trim() || null; + const devices = await this.client.getDevices(); + const entries = Array.isArray(devices?.devices) ? devices.devices : []; + return entries.map((device) => ({ + deviceId: device.device_id, + displayName: device.display_name?.trim() || null, + lastSeenIp: device.last_seen_ip?.trim() || null, + lastSeenTs: + typeof device.last_seen_ts === "number" && Number.isFinite(device.last_seen_ts) + ? device.last_seen_ts + : null, + current: currentDeviceId !== null && device.device_id === currentDeviceId, + })); + } + + async deleteOwnDevices(deviceIds: string[]): Promise { + const uniqueDeviceIds = [...new Set(deviceIds.map((value) => value.trim()).filter(Boolean))]; + const currentDeviceId = this.client.getDeviceId()?.trim() || null; + const protectedDeviceIds = uniqueDeviceIds.filter((deviceId) => deviceId === currentDeviceId); + if (protectedDeviceIds.length > 0) { + throw new Error(`Refusing to delete the current Matrix device: ${protectedDeviceIds[0]}`); + } + + const deleteWithAuth = async (authData?: Record): Promise => { + await this.client.deleteMultipleDevices(uniqueDeviceIds, authData as never); + }; + + if (uniqueDeviceIds.length > 0) { + try { + await deleteWithAuth(); + } catch (err) { + const session = + err && + typeof err === "object" && + "data" in err && + err.data && + typeof err.data === "object" && + "session" in err.data && + typeof err.data.session === "string" + ? err.data.session + : null; + const userId = await this.getUserId().catch(() => this.selfUserId); + if (!session || !userId || !this.password?.trim()) { + throw err; + } + await deleteWithAuth({ + type: "m.login.password", + session, + identifier: { type: "m.id.user", user: userId }, + password: this.password, + }); + } + } + + return { + currentDeviceId, + deletedDeviceIds: uniqueDeviceIds, + remainingDevices: await this.listOwnDevices(), + }; + } + private async resolveActiveRoomKeyBackupVersion( crypto: MatrixCryptoBootstrapApi, ): Promise {