From 7a44a6370d644a564e035ce0b8e8f319b9037add Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 25 Feb 2026 15:10:26 -0500 Subject: [PATCH] matrix-js: stabilize verification flow and rename verify device command --- docs/channels/matrix-js.md | 4 +- extensions/matrix-js/src/cli.test.ts | 82 +++++++++++++++++ extensions/matrix-js/src/cli.ts | 43 +++++++-- .../src/matrix/actions/client.test.ts | 87 +++++++++++++++++++ .../matrix-js/src/matrix/actions/client.ts | 27 +----- .../src/matrix/client/create-client.ts | 2 + .../matrix-js/src/matrix/monitor/index.ts | 2 +- extensions/matrix-js/src/matrix/sdk.test.ts | 42 +++++++++ extensions/matrix-js/src/matrix/sdk.ts | 32 +++++-- .../matrix-js/src/matrix/send/client.test.ts | 78 +++++++++++++++++ .../matrix-js/src/matrix/send/client.ts | 26 +----- 11 files changed, 362 insertions(+), 63 deletions(-) create mode 100644 extensions/matrix-js/src/cli.test.ts create mode 100644 extensions/matrix-js/src/matrix/actions/client.test.ts create mode 100644 extensions/matrix-js/src/matrix/send/client.test.ts diff --git a/docs/channels/matrix-js.md b/docs/channels/matrix-js.md index 5f677341358..9d9949d084d 100644 --- a/docs/channels/matrix-js.md +++ b/docs/channels/matrix-js.md @@ -147,10 +147,10 @@ Bootstrap cross-signing and verification state: openclaw matrix-js verify bootstrap ``` -Verify with a recovery key: +Verify this device with a recovery key: ```bash -openclaw matrix-js verify recovery-key "" +openclaw matrix-js verify device "" ``` Use `openclaw matrix-js verify status --json` when scripting verification checks. diff --git a/extensions/matrix-js/src/cli.test.ts b/extensions/matrix-js/src/cli.test.ts new file mode 100644 index 00000000000..580b48be8de --- /dev/null +++ b/extensions/matrix-js/src/cli.test.ts @@ -0,0 +1,82 @@ +import { Command } from "commander"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const bootstrapMatrixVerificationMock = vi.fn(); +const getMatrixVerificationStatusMock = vi.fn(); +const verifyMatrixRecoveryKeyMock = vi.fn(); + +vi.mock("./matrix/actions/verification.js", () => ({ + bootstrapMatrixVerification: (...args: unknown[]) => bootstrapMatrixVerificationMock(...args), + getMatrixVerificationStatus: (...args: unknown[]) => getMatrixVerificationStatusMock(...args), + verifyMatrixRecoveryKey: (...args: unknown[]) => verifyMatrixRecoveryKeyMock(...args), +})); + +let registerMatrixJsCli: typeof import("./cli.js").registerMatrixJsCli; + +function buildProgram(): Command { + const program = new Command(); + registerMatrixJsCli({ program }); + return program; +} + +describe("matrix-js CLI verification commands", () => { + beforeEach(async () => { + vi.resetModules(); + vi.clearAllMocks(); + process.exitCode = undefined; + ({ registerMatrixJsCli } = await import("./cli.js")); + vi.spyOn(console, "log").mockImplementation(() => {}); + vi.spyOn(console, "error").mockImplementation(() => {}); + }); + + afterEach(() => { + vi.restoreAllMocks(); + process.exitCode = undefined; + }); + + it("sets non-zero exit code for device verification failures in JSON mode", async () => { + verifyMatrixRecoveryKeyMock.mockResolvedValue({ + success: false, + error: "invalid key", + }); + const program = buildProgram(); + + await program.parseAsync(["matrix-js", "verify", "device", "bad-key", "--json"], { + from: "user", + }); + + expect(process.exitCode).toBe(1); + }); + + it("sets non-zero exit code for bootstrap failures in JSON mode", async () => { + bootstrapMatrixVerificationMock.mockResolvedValue({ + success: false, + error: "bootstrap failed", + verification: {}, + crossSigning: {}, + pendingVerifications: 0, + cryptoBootstrap: null, + }); + const program = buildProgram(); + + await program.parseAsync(["matrix-js", "verify", "bootstrap", "--json"], { from: "user" }); + + expect(process.exitCode).toBe(1); + }); + + it("keeps zero exit code for successful bootstrap in JSON mode", async () => { + process.exitCode = 0; + bootstrapMatrixVerificationMock.mockResolvedValue({ + success: true, + verification: {}, + crossSigning: {}, + pendingVerifications: 0, + cryptoBootstrap: {}, + }); + const program = buildProgram(); + + await program.parseAsync(["matrix-js", "verify", "bootstrap", "--json"], { from: "user" }); + + expect(process.exitCode).toBe(0); + }); +}); diff --git a/extensions/matrix-js/src/cli.ts b/extensions/matrix-js/src/cli.ts index ee618805c8d..a5094d1442c 100644 --- a/extensions/matrix-js/src/cli.ts +++ b/extensions/matrix-js/src/cli.ts @@ -5,6 +5,23 @@ import { verifyMatrixRecoveryKey, } from "./matrix/actions/verification.js"; +let matrixJsCliExitScheduled = false; + +function scheduleMatrixJsCliExit(): void { + if (matrixJsCliExitScheduled || process.env.VITEST) { + return; + } + matrixJsCliExitScheduled = true; + // matrix-js-sdk rust crypto can leave background async work alive after command completion. + setTimeout(() => { + process.exit(process.exitCode ?? 0); + }, 0); +} + +function markCliFailure(): void { + process.exitCode = 1; +} + function printVerificationStatus(status: { verified: boolean; userId: string | null; @@ -25,7 +42,7 @@ function printVerificationStatus(status: { console.log("Verified: no"); console.log(`User: ${status.userId ?? "unknown"}`); console.log(`Device: ${status.deviceId ?? "unknown"}`); - console.log("Run 'openclaw matrix-js verify recovery-key ' to verify this device."); + console.log("Run 'openclaw matrix-js verify device ' to verify this device."); } console.log(`Recovery key stored: ${status.recoveryKeyStored ? "yes" : "no"}`); if (status.recoveryKeyCreatedAt) { @@ -66,7 +83,9 @@ export function registerMatrixJsCli(params: { program: Command }): void { } else { console.error(`Error: ${message}`); } - process.exitCode = 1; + markCliFailure(); + } finally { + scheduleMatrixJsCliExit(); } }); @@ -92,6 +111,9 @@ export function registerMatrixJsCli(params: { program: Command }): void { }); if (options.json) { console.log(JSON.stringify(result, null, 2)); + if (!result.success) { + markCliFailure(); + } return; } console.log(`Bootstrap success: ${result.success ? "yes" : "no"}`); @@ -106,7 +128,7 @@ export function registerMatrixJsCli(params: { program: Command }): void { ); console.log(`Pending verifications: ${result.pendingVerifications}`); if (!result.success) { - process.exitCode = 1; + markCliFailure(); } } catch (err) { const message = err instanceof Error ? err.message : String(err); @@ -115,13 +137,15 @@ export function registerMatrixJsCli(params: { program: Command }): void { } else { console.error(`Verification bootstrap failed: ${message}`); } - process.exitCode = 1; + markCliFailure(); + } finally { + scheduleMatrixJsCliExit(); } }, ); verify - .command("recovery-key ") + .command("device ") .description("Verify device using a Matrix recovery key") .option("--account ", "Account ID (for multi-account setups)") .option("--json", "Output as JSON") @@ -130,6 +154,9 @@ export function registerMatrixJsCli(params: { program: Command }): void { const result = await verifyMatrixRecoveryKey(key, { accountId: options.account }); if (options.json) { console.log(JSON.stringify(result, null, 2)); + if (!result.success) { + markCliFailure(); + } } else if (result.success) { console.log("Device verification completed successfully."); console.log(`User: ${result.userId ?? "unknown"}`); @@ -139,7 +166,7 @@ export function registerMatrixJsCli(params: { program: Command }): void { } } else { console.error(`Verification failed: ${result.error ?? "unknown error"}`); - process.exitCode = 1; + markCliFailure(); } } catch (err) { const message = err instanceof Error ? err.message : String(err); @@ -148,7 +175,9 @@ export function registerMatrixJsCli(params: { program: Command }): void { } else { console.error(`Verification failed: ${message}`); } - process.exitCode = 1; + markCliFailure(); + } finally { + scheduleMatrixJsCliExit(); } }); } diff --git a/extensions/matrix-js/src/matrix/actions/client.test.ts b/extensions/matrix-js/src/matrix/actions/client.test.ts new file mode 100644 index 00000000000..b486e89cf29 --- /dev/null +++ b/extensions/matrix-js/src/matrix/actions/client.test.ts @@ -0,0 +1,87 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import type { MatrixClient } from "../sdk.js"; + +const loadConfigMock = vi.fn(() => ({})); +const getActiveMatrixClientMock = vi.fn(); +const createMatrixClientMock = vi.fn(); +const isBunRuntimeMock = vi.fn(() => false); +const resolveMatrixAuthMock = vi.fn(); + +vi.mock("../../runtime.js", () => ({ + getMatrixRuntime: () => ({ + config: { + loadConfig: (...args: unknown[]) => loadConfigMock(...args), + }, + }), +})); + +vi.mock("../active-client.js", () => ({ + getActiveMatrixClient: (...args: unknown[]) => getActiveMatrixClientMock(...args), +})); + +vi.mock("../client.js", () => ({ + createMatrixClient: (...args: unknown[]) => createMatrixClientMock(...args), + isBunRuntime: () => isBunRuntimeMock(), + resolveMatrixAuth: (...args: unknown[]) => resolveMatrixAuthMock(...args), +})); + +let resolveActionClient: typeof import("./client.js").resolveActionClient; + +function createMockMatrixClient(): MatrixClient { + return { + prepareForOneOff: vi.fn(async () => undefined), + } as unknown as MatrixClient; +} + +describe("resolveActionClient", () => { + beforeEach(async () => { + vi.resetModules(); + vi.clearAllMocks(); + getActiveMatrixClientMock.mockReturnValue(null); + isBunRuntimeMock.mockReturnValue(false); + resolveMatrixAuthMock.mockResolvedValue({ + homeserver: "https://matrix.example.org", + userId: "@bot:example.org", + accessToken: "token", + password: undefined, + deviceId: "DEVICE123", + encryption: false, + }); + createMatrixClientMock.mockResolvedValue(createMockMatrixClient()); + + ({ resolveActionClient } = await import("./client.js")); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + }); + + it("creates a one-off client even when OPENCLAW_GATEWAY_PORT is set", async () => { + vi.stubEnv("OPENCLAW_GATEWAY_PORT", "18799"); + + const result = await resolveActionClient({ accountId: "default" }); + + expect(getActiveMatrixClientMock).toHaveBeenCalledWith("default"); + expect(resolveMatrixAuthMock).toHaveBeenCalledTimes(1); + expect(createMatrixClientMock).toHaveBeenCalledTimes(1); + expect(createMatrixClientMock).toHaveBeenCalledWith( + expect.objectContaining({ + autoBootstrapCrypto: false, + }), + ); + const oneOffClient = await createMatrixClientMock.mock.results[0]?.value; + expect(oneOffClient.prepareForOneOff).toHaveBeenCalledTimes(1); + expect(result.stopOnDone).toBe(true); + }); + + it("reuses active monitor client when available", async () => { + const activeClient = createMockMatrixClient(); + getActiveMatrixClientMock.mockReturnValue(activeClient); + + const result = await resolveActionClient({ accountId: "default" }); + + expect(result).toEqual({ client: activeClient, stopOnDone: false }); + expect(resolveMatrixAuthMock).not.toHaveBeenCalled(); + expect(createMatrixClientMock).not.toHaveBeenCalled(); + }); +}); diff --git a/extensions/matrix-js/src/matrix/actions/client.ts b/extensions/matrix-js/src/matrix/actions/client.ts index 108be18aafd..2550f0bdfee 100644 --- a/extensions/matrix-js/src/matrix/actions/client.ts +++ b/extensions/matrix-js/src/matrix/actions/client.ts @@ -1,11 +1,6 @@ import { getMatrixRuntime } from "../../runtime.js"; import { getActiveMatrixClient } from "../active-client.js"; -import { - createMatrixClient, - isBunRuntime, - resolveMatrixAuth, - resolveSharedMatrixClient, -} from "../client.js"; +import { createMatrixClient, isBunRuntime, resolveMatrixAuth } from "../client.js"; import type { CoreConfig } from "../types.js"; import type { MatrixActionClient, MatrixActionClientOpts } from "./types.js"; @@ -26,15 +21,6 @@ export async function resolveActionClient( if (active) { return { client: active, stopOnDone: false }; } - const shouldShareClient = Boolean(process.env.OPENCLAW_GATEWAY_PORT); - if (shouldShareClient) { - const client = await resolveSharedMatrixClient({ - cfg: getMatrixRuntime().config.loadConfig() as CoreConfig, - timeoutMs: opts.timeoutMs, - accountId: opts.accountId, - }); - return { client, stopOnDone: false }; - } const auth = await resolveMatrixAuth({ cfg: getMatrixRuntime().config.loadConfig() as CoreConfig, accountId: opts.accountId, @@ -48,15 +34,8 @@ export async function resolveActionClient( encryption: auth.encryption, localTimeoutMs: opts.timeoutMs, accountId: opts.accountId, + autoBootstrapCrypto: false, }); - if (auth.encryption && client.crypto) { - try { - const joinedRooms = await client.getJoinedRooms(); - await client.crypto.prepare(joinedRooms); - } catch { - // Ignore crypto prep failures for one-off actions. - } - } - await client.start(); + await client.prepareForOneOff(); return { client, stopOnDone: true }; } diff --git a/extensions/matrix-js/src/matrix/client/create-client.ts b/extensions/matrix-js/src/matrix/client/create-client.ts index f349a0565a9..3a1d996233f 100644 --- a/extensions/matrix-js/src/matrix/client/create-client.ts +++ b/extensions/matrix-js/src/matrix/client/create-client.ts @@ -17,6 +17,7 @@ export async function createMatrixClient(params: { localTimeoutMs?: number; initialSyncLimit?: number; accountId?: string | null; + autoBootstrapCrypto?: boolean; }): Promise { ensureMatrixSdkLoggingConfigured(); const env = process.env; @@ -52,5 +53,6 @@ export async function createMatrixClient(params: { recoveryKeyPath: storagePaths.recoveryKeyPath, idbSnapshotPath: storagePaths.idbSnapshotPath, cryptoDatabasePrefix, + autoBootstrapCrypto: params.autoBootstrapCrypto, }); } diff --git a/extensions/matrix-js/src/matrix/monitor/index.ts b/extensions/matrix-js/src/matrix/monitor/index.ts index b263113346c..64e9fcd4a33 100644 --- a/extensions/matrix-js/src/matrix/monitor/index.ts +++ b/extensions/matrix-js/src/matrix/monitor/index.ts @@ -337,7 +337,7 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi logger.info("matrix: device is verified and ready for encrypted rooms"); } else { logger.info( - "matrix: device not verified — run 'openclaw matrix-js verify recovery-key ' to enable E2EE", + "matrix: device not verified — run 'openclaw matrix-js verify device ' to enable E2EE", ); } } catch (err) { diff --git a/extensions/matrix-js/src/matrix/sdk.test.ts b/extensions/matrix-js/src/matrix/sdk.test.ts index 2119d689a26..60f9c817a21 100644 --- a/extensions/matrix-js/src/matrix/sdk.test.ts +++ b/extensions/matrix-js/src/matrix/sdk.test.ts @@ -947,4 +947,46 @@ describe("MatrixClient crypto bootstrapping", () => { expect(result.crossSigning.published).toBe(true); expect(result.cryptoBootstrap).not.toBeNull(); }); + + it("does not report bootstrap errors when final verification state is healthy", async () => { + matrixJsClient.getUserId = vi.fn(() => "@bot:example.org"); + matrixJsClient.getDeviceId = vi.fn(() => "DEVICE123"); + matrixJsClient.getCrypto = vi.fn(() => ({ + on: vi.fn(), + bootstrapCrossSigning: vi.fn(async () => {}), + bootstrapSecretStorage: vi.fn(async () => {}), + requestOwnUserVerification: vi.fn(async () => null), + isCrossSigningReady: vi.fn(async () => true), + userHasCrossSigningKeys: vi.fn(async () => true), + getSecretStorageStatus: vi.fn(async () => ({ + ready: true, + defaultKeyId: "SSSSKEY", + secretStorageKeyValidityMap: { SSSSKEY: true }, + })), + getDeviceVerificationStatus: vi.fn(async () => ({ + isVerified: () => true, + localVerified: true, + crossSigningVerified: true, + signedByOwner: true, + })), + })); + + const client = new MatrixClient("https://matrix.example.org", "token", undefined, undefined, { + encryption: true, + }); + vi.spyOn(client, "getOwnCrossSigningPublicationStatus").mockResolvedValue({ + userId: "@bot:example.org", + masterKeyPublished: true, + selfSigningKeyPublished: true, + userSigningKeyPublished: true, + published: true, + }); + + const result = await client.bootstrapOwnDeviceVerification({ + recoveryKey: "not-a-valid-recovery-key", + }); + + expect(result.success).toBe(true); + expect(result.error).toBeUndefined(); + }); }); diff --git a/extensions/matrix-js/src/matrix/sdk.ts b/extensions/matrix-js/src/matrix/sdk.ts index 8cb04da6689..abd635195e9 100644 --- a/extensions/matrix-js/src/matrix/sdk.ts +++ b/extensions/matrix-js/src/matrix/sdk.ts @@ -109,6 +109,7 @@ export class MatrixClient { private readonly verificationManager = new MatrixVerificationManager(); private readonly recoveryKeyStore: MatrixRecoveryKeyStore; private readonly cryptoBootstrapper: MatrixCryptoBootstrapper; + private readonly autoBootstrapCrypto: boolean; readonly dms = { update: async (): Promise => { @@ -134,6 +135,7 @@ export class MatrixClient { recoveryKeyPath?: string; idbSnapshotPath?: string; cryptoDatabasePrefix?: string; + autoBootstrapCrypto?: boolean; } = {}, ) { this.httpClient = new MatrixAuthedHttpClient(homeserver, accessToken); @@ -144,6 +146,7 @@ export class MatrixClient { this.idbSnapshotPath = opts.idbSnapshotPath; this.cryptoDatabasePrefix = opts.cryptoDatabasePrefix; this.selfUserId = opts.userId?.trim() || null; + this.autoBootstrapCrypto = opts.autoBootstrapCrypto !== false; this.recoveryKeyStore = new MatrixRecoveryKeyStore(opts.recoveryKeyPath); const cryptoCallbacks = this.encryptionEnabled ? this.recoveryKeyStore.buildCryptoCallbacks() @@ -229,12 +232,30 @@ export class MatrixClient { await this.client.startClient({ initialSyncLimit: this.initialSyncLimit, }); - await this.bootstrapCryptoIfNeeded(); + if (this.autoBootstrapCrypto) { + await this.bootstrapCryptoIfNeeded(); + } this.started = true; this.emitOutstandingInviteEvents(); await this.refreshDmCache().catch(noop); } + async prepareForOneOff(): Promise { + if (!this.encryptionEnabled) { + return; + } + await this.initializeCryptoIfNeeded(); + if (!this.crypto) { + return; + } + try { + const joinedRooms = await this.getJoinedRooms(); + await this.crypto.prepare(joinedRooms); + } catch { + // One-off commands should continue even if crypto room prep is incomplete. + } + } + stop(): void { if (this.idbPersistTimer) { clearInterval(this.idbPersistTimer); @@ -709,11 +730,10 @@ export class MatrixClient { const verification = await this.getOwnDeviceVerificationStatus(); const crossSigning = await this.getOwnCrossSigningPublicationStatus(); const success = verification.verified && crossSigning.published; - const error = - bootstrapError ?? - (success - ? undefined - : "Matrix verification bootstrap did not produce a verified device with published cross-signing keys"); + const error = success + ? undefined + : (bootstrapError ?? + "Matrix verification bootstrap did not produce a verified device with published cross-signing keys"); return { success, error, diff --git a/extensions/matrix-js/src/matrix/send/client.test.ts b/extensions/matrix-js/src/matrix/send/client.test.ts new file mode 100644 index 00000000000..2f839a7f1ae --- /dev/null +++ b/extensions/matrix-js/src/matrix/send/client.test.ts @@ -0,0 +1,78 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import type { MatrixClient } from "../sdk.js"; + +const getActiveMatrixClientMock = vi.fn(); +const createMatrixClientMock = vi.fn(); +const isBunRuntimeMock = vi.fn(() => false); +const resolveMatrixAuthMock = vi.fn(); + +vi.mock("../active-client.js", () => ({ + getActiveMatrixClient: (...args: unknown[]) => getActiveMatrixClientMock(...args), +})); + +vi.mock("../client.js", () => ({ + createMatrixClient: (...args: unknown[]) => createMatrixClientMock(...args), + isBunRuntime: () => isBunRuntimeMock(), + resolveMatrixAuth: (...args: unknown[]) => resolveMatrixAuthMock(...args), +})); + +let resolveMatrixClient: typeof import("./client.js").resolveMatrixClient; + +function createMockMatrixClient(): MatrixClient { + return { + prepareForOneOff: vi.fn(async () => undefined), + } as unknown as MatrixClient; +} + +describe("resolveMatrixClient", () => { + beforeEach(async () => { + vi.resetModules(); + vi.clearAllMocks(); + getActiveMatrixClientMock.mockReturnValue(null); + isBunRuntimeMock.mockReturnValue(false); + resolveMatrixAuthMock.mockResolvedValue({ + homeserver: "https://matrix.example.org", + userId: "@bot:example.org", + accessToken: "token", + password: undefined, + deviceId: "DEVICE123", + encryption: false, + }); + createMatrixClientMock.mockResolvedValue(createMockMatrixClient()); + + ({ resolveMatrixClient } = await import("./client.js")); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + }); + + it("creates a one-off client even when OPENCLAW_GATEWAY_PORT is set", async () => { + vi.stubEnv("OPENCLAW_GATEWAY_PORT", "18799"); + + const result = await resolveMatrixClient({ accountId: "default" }); + + expect(getActiveMatrixClientMock).toHaveBeenCalledWith("default"); + expect(resolveMatrixAuthMock).toHaveBeenCalledTimes(1); + expect(createMatrixClientMock).toHaveBeenCalledTimes(1); + expect(createMatrixClientMock).toHaveBeenCalledWith( + expect.objectContaining({ + autoBootstrapCrypto: false, + }), + ); + const oneOffClient = await createMatrixClientMock.mock.results[0]?.value; + expect(oneOffClient.prepareForOneOff).toHaveBeenCalledTimes(1); + expect(result.stopOnDone).toBe(true); + }); + + it("reuses active monitor client when available", async () => { + const activeClient = createMockMatrixClient(); + getActiveMatrixClientMock.mockReturnValue(activeClient); + + const result = await resolveMatrixClient({ accountId: "default" }); + + expect(result).toEqual({ client: activeClient, stopOnDone: false }); + expect(resolveMatrixAuthMock).not.toHaveBeenCalled(); + expect(createMatrixClientMock).not.toHaveBeenCalled(); + }); +}); diff --git a/extensions/matrix-js/src/matrix/send/client.ts b/extensions/matrix-js/src/matrix/send/client.ts index d68d35e4edd..5a5cf71a391 100644 --- a/extensions/matrix-js/src/matrix/send/client.ts +++ b/extensions/matrix-js/src/matrix/send/client.ts @@ -1,11 +1,6 @@ import { getMatrixRuntime } from "../../runtime.js"; import { getActiveMatrixClient } from "../active-client.js"; -import { - createMatrixClient, - isBunRuntime, - resolveMatrixAuth, - resolveSharedMatrixClient, -} from "../client.js"; +import { createMatrixClient, isBunRuntime, resolveMatrixAuth } from "../client.js"; import type { MatrixClient } from "../sdk.js"; import type { CoreConfig } from "../types.js"; @@ -38,14 +33,6 @@ export async function resolveMatrixClient(opts: { if (active) { return { client: active, stopOnDone: false }; } - const shouldShareClient = Boolean(process.env.OPENCLAW_GATEWAY_PORT); - if (shouldShareClient) { - const client = await resolveSharedMatrixClient({ - timeoutMs: opts.timeoutMs, - accountId: opts.accountId, - }); - return { client, stopOnDone: false }; - } const auth = await resolveMatrixAuth({ accountId: opts.accountId }); const client = await createMatrixClient({ homeserver: auth.homeserver, @@ -56,15 +43,8 @@ export async function resolveMatrixClient(opts: { encryption: auth.encryption, localTimeoutMs: opts.timeoutMs, accountId: opts.accountId, + autoBootstrapCrypto: false, }); - if (auth.encryption && client.crypto) { - try { - const joinedRooms = await client.getJoinedRooms(); - await client.crypto.prepare(joinedRooms); - } catch { - // Ignore crypto prep failures for one-off sends; normal sync will retry. - } - } - await client.start(); + await client.prepareForOneOff(); return { client, stopOnDone: true }; }