diff --git a/extensions/matrix/src/cli.test.ts b/extensions/matrix/src/cli.test.ts index 5fc474a6538..3be18e47a90 100644 --- a/extensions/matrix/src/cli.test.ts +++ b/extensions/matrix/src/cli.test.ts @@ -12,6 +12,7 @@ const matrixSetupValidateInputMock = vi.fn(); const matrixRuntimeLoadConfigMock = vi.fn(); const matrixRuntimeWriteConfigFileMock = vi.fn(); const restoreMatrixRoomKeyBackupMock = vi.fn(); +const setMatrixSdkConsoleLoggingMock = vi.fn(); const setMatrixSdkLogModeMock = vi.fn(); const updateMatrixOwnProfileMock = vi.fn(); const verifyMatrixRecoveryKeyMock = vi.fn(); @@ -25,6 +26,7 @@ vi.mock("./matrix/actions/verification.js", () => ({ })); vi.mock("./matrix/client/logging.js", () => ({ + setMatrixSdkConsoleLogging: (...args: unknown[]) => setMatrixSdkConsoleLoggingMock(...args), setMatrixSdkLogMode: (...args: unknown[]) => setMatrixSdkLogModeMock(...args), })); diff --git a/extensions/matrix/src/cli.ts b/extensions/matrix/src/cli.ts index 6a36082d8d5..163dab0afd3 100644 --- a/extensions/matrix/src/cli.ts +++ b/extensions/matrix/src/cli.ts @@ -14,7 +14,7 @@ import { restoreMatrixRoomKeyBackup, verifyMatrixRecoveryKey, } from "./matrix/actions/verification.js"; -import { setMatrixSdkLogMode } from "./matrix/client/logging.js"; +import { setMatrixSdkConsoleLogging, setMatrixSdkLogMode } from "./matrix/client/logging.js"; import { resolveMatrixConfigPath, updateMatrixAccountConfig } from "./matrix/config-update.js"; import { applyMatrixProfileUpdate, type MatrixProfileUpdateResult } from "./profile-update.js"; import { getMatrixRuntime } from "./runtime.js"; @@ -69,6 +69,7 @@ function printAccountLabel(accountId?: string): void { function configureCliLogMode(verbose: boolean): void { setMatrixSdkLogMode(verbose ? "default" : "quiet"); + setMatrixSdkConsoleLogging(verbose); } function parseOptionalInt(value: string | undefined, fieldName: string): number | undefined { diff --git a/extensions/matrix/src/matrix/actions/client.test.ts b/extensions/matrix/src/matrix/actions/client.test.ts index 10768836a95..7a397cb27f2 100644 --- a/extensions/matrix/src/matrix/actions/client.test.ts +++ b/extensions/matrix/src/matrix/actions/client.test.ts @@ -32,6 +32,7 @@ let resolveActionClient: typeof import("./client.js").resolveActionClient; function createMockMatrixClient(): MatrixClient { return { prepareForOneOff: vi.fn(async () => undefined), + start: vi.fn(async () => undefined), } as unknown as MatrixClient; } @@ -92,6 +93,30 @@ describe("resolveActionClient", () => { expect(result.stopOnDone).toBe(true); }); + it("skips one-off room preparation when readiness is disabled", async () => { + const result = await resolveActionClient({ + accountId: "default", + readiness: "none", + }); + + const oneOffClient = await createMatrixClientMock.mock.results[0]?.value; + expect(oneOffClient.prepareForOneOff).not.toHaveBeenCalled(); + expect(oneOffClient.start).not.toHaveBeenCalled(); + expect(result.stopOnDone).toBe(true); + }); + + it("starts one-off clients when started readiness is required", async () => { + const result = await resolveActionClient({ + accountId: "default", + readiness: "started", + }); + + const oneOffClient = await createMatrixClientMock.mock.results[0]?.value; + expect(oneOffClient.start).toHaveBeenCalledTimes(1); + expect(oneOffClient.prepareForOneOff).not.toHaveBeenCalled(); + expect(result.stopOnDone).toBe(true); + }); + it("reuses active monitor client when available", async () => { const activeClient = createMockMatrixClient(); getActiveMatrixClientMock.mockReturnValue(activeClient); @@ -103,6 +128,20 @@ describe("resolveActionClient", () => { expect(createMatrixClientMock).not.toHaveBeenCalled(); }); + it("starts active clients when started readiness is required", async () => { + const activeClient = createMockMatrixClient(); + getActiveMatrixClientMock.mockReturnValue(activeClient); + + const result = await resolveActionClient({ + accountId: "default", + readiness: "started", + }); + + expect(result).toEqual({ client: activeClient, stopOnDone: false }); + expect(activeClient.start).toHaveBeenCalledTimes(1); + expect(activeClient.prepareForOneOff).not.toHaveBeenCalled(); + }); + it("uses the implicit resolved account id for active client lookup and storage", async () => { loadConfigMock.mockReturnValue({ channels: { diff --git a/extensions/matrix/src/matrix/actions/client.ts b/extensions/matrix/src/matrix/actions/client.ts index 5bd72347fac..49450b61cee 100644 --- a/extensions/matrix/src/matrix/actions/client.ts +++ b/extensions/matrix/src/matrix/actions/client.ts @@ -15,11 +15,28 @@ export function ensureNodeRuntime() { } } +async function ensureActionClientReadiness( + client: MatrixActionClient["client"], + readiness: MatrixActionClientOpts["readiness"], + opts: { createdForOneOff: boolean }, +): Promise { + if (readiness === "started") { + await client.start(); + return; + } + if (readiness === "prepared" || (!readiness && opts.createdForOneOff)) { + await client.prepareForOneOff(); + } +} + export async function resolveActionClient( opts: MatrixActionClientOpts = {}, ): Promise { ensureNodeRuntime(); if (opts.client) { + await ensureActionClientReadiness(opts.client, opts.readiness, { + createdForOneOff: false, + }); return { client: opts.client, stopOnDone: false }; } const cfg = getMatrixRuntime().config.loadConfig() as CoreConfig; @@ -29,6 +46,9 @@ export async function resolveActionClient( }); const active = getActiveMatrixClient(authContext.accountId); if (active) { + await ensureActionClientReadiness(active, opts.readiness, { + createdForOneOff: false, + }); return { client: active, stopOnDone: false }; } const auth = await resolveMatrixAuth({ @@ -46,7 +66,9 @@ export async function resolveActionClient( accountId: auth.accountId, autoBootstrapCrypto: false, }); - await client.prepareForOneOff(); + await ensureActionClientReadiness(client, opts.readiness, { + createdForOneOff: true, + }); return { client, stopOnDone: true }; } diff --git a/extensions/matrix/src/matrix/actions/types.ts b/extensions/matrix/src/matrix/actions/types.ts index 57092550d30..c27f090ce19 100644 --- a/extensions/matrix/src/matrix/actions/types.ts +++ b/extensions/matrix/src/matrix/actions/types.ts @@ -48,6 +48,7 @@ export type MatrixActionClientOpts = { client?: MatrixClient; timeoutMs?: number; accountId?: string | null; + readiness?: "none" | "prepared" | "started"; }; export type MatrixMessageSummary = { diff --git a/extensions/matrix/src/matrix/actions/verification.ts b/extensions/matrix/src/matrix/actions/verification.ts index 64e7118118a..937b5992bb7 100644 --- a/extensions/matrix/src/matrix/actions/verification.ts +++ b/extensions/matrix/src/matrix/actions/verification.ts @@ -20,7 +20,7 @@ function resolveVerificationId(input: string): string { export async function listMatrixVerifications(opts: MatrixActionClientOpts = {}) { return await withResolvedActionClient( - opts, + { ...opts, readiness: "started" }, async (client) => { const crypto = requireCrypto(client); return await crypto.listVerifications(); @@ -38,7 +38,7 @@ export async function requestMatrixVerification( } = {}, ) { return await withResolvedActionClient( - params, + { ...params, readiness: "started" }, async (client) => { const crypto = requireCrypto(client); const ownUser = params.ownUser ?? (!params.userId && !params.deviceId && !params.roomId); @@ -58,7 +58,7 @@ export async function acceptMatrixVerification( opts: MatrixActionClientOpts = {}, ) { return await withResolvedActionClient( - opts, + { ...opts, readiness: "started" }, async (client) => { const crypto = requireCrypto(client); return await crypto.acceptVerification(resolveVerificationId(requestId)); @@ -72,7 +72,7 @@ export async function cancelMatrixVerification( opts: MatrixActionClientOpts & { reason?: string; code?: string } = {}, ) { return await withResolvedActionClient( - opts, + { ...opts, readiness: "started" }, async (client) => { const crypto = requireCrypto(client); return await crypto.cancelVerification(resolveVerificationId(requestId), { @@ -89,7 +89,7 @@ export async function startMatrixVerification( opts: MatrixActionClientOpts & { method?: "sas" } = {}, ) { return await withResolvedActionClient( - opts, + { ...opts, readiness: "started" }, async (client) => { const crypto = requireCrypto(client); return await crypto.startVerification(resolveVerificationId(requestId), opts.method ?? "sas"); @@ -103,7 +103,7 @@ export async function generateMatrixVerificationQr( opts: MatrixActionClientOpts = {}, ) { return await withResolvedActionClient( - opts, + { ...opts, readiness: "started" }, async (client) => { const crypto = requireCrypto(client); return await crypto.generateVerificationQr(resolveVerificationId(requestId)); @@ -118,7 +118,7 @@ export async function scanMatrixVerificationQr( opts: MatrixActionClientOpts = {}, ) { return await withResolvedActionClient( - opts, + { ...opts, readiness: "started" }, async (client) => { const crypto = requireCrypto(client); const payload = qrDataBase64.trim(); @@ -136,7 +136,7 @@ export async function getMatrixVerificationSas( opts: MatrixActionClientOpts = {}, ) { return await withResolvedActionClient( - opts, + { ...opts, readiness: "started" }, async (client) => { const crypto = requireCrypto(client); return await crypto.getVerificationSas(resolveVerificationId(requestId)); @@ -150,7 +150,7 @@ export async function confirmMatrixVerificationSas( opts: MatrixActionClientOpts = {}, ) { return await withResolvedActionClient( - opts, + { ...opts, readiness: "started" }, async (client) => { const crypto = requireCrypto(client); return await crypto.confirmVerificationSas(resolveVerificationId(requestId)); @@ -164,7 +164,7 @@ export async function mismatchMatrixVerificationSas( opts: MatrixActionClientOpts = {}, ) { return await withResolvedActionClient( - opts, + { ...opts, readiness: "started" }, async (client) => { const crypto = requireCrypto(client); return await crypto.mismatchVerificationSas(resolveVerificationId(requestId)); @@ -178,7 +178,7 @@ export async function confirmMatrixVerificationReciprocateQr( opts: MatrixActionClientOpts = {}, ) { return await withResolvedActionClient( - opts, + { ...opts, readiness: "started" }, async (client) => { const crypto = requireCrypto(client); return await crypto.confirmVerificationReciprocateQr(resolveVerificationId(requestId)); @@ -191,7 +191,7 @@ export async function getMatrixEncryptionStatus( opts: MatrixActionClientOpts & { includeRecoveryKey?: boolean } = {}, ) { return await withResolvedActionClient( - opts, + { ...opts, readiness: "started" }, async (client) => { const crypto = requireCrypto(client); const recoveryKey = await crypto.getRecoveryKey(); @@ -211,7 +211,7 @@ export async function getMatrixVerificationStatus( opts: MatrixActionClientOpts & { includeRecoveryKey?: boolean } = {}, ) { return await withResolvedActionClient( - opts, + { ...opts, readiness: "started" }, async (client) => { const status = await client.getOwnDeviceVerificationStatus(); const payload = { @@ -233,7 +233,7 @@ export async function getMatrixVerificationStatus( export async function getMatrixRoomKeyBackupStatus(opts: MatrixActionClientOpts = {}) { return await withResolvedActionClient( - opts, + { ...opts, readiness: "started" }, async (client) => await client.getRoomKeyBackupStatus(), "persist", ); @@ -244,7 +244,7 @@ export async function verifyMatrixRecoveryKey( opts: MatrixActionClientOpts = {}, ) { return await withResolvedActionClient( - opts, + { ...opts, readiness: "started" }, async (client) => await client.verifyWithRecoveryKey(recoveryKey), "persist", ); @@ -256,7 +256,7 @@ export async function restoreMatrixRoomKeyBackup( } = {}, ) { return await withResolvedActionClient( - opts, + { ...opts, readiness: "started" }, async (client) => await client.restoreRoomKeyBackup({ recoveryKey: opts.recoveryKey?.trim() || undefined, @@ -272,7 +272,7 @@ export async function bootstrapMatrixVerification( } = {}, ) { return await withResolvedActionClient( - opts, + { ...opts, readiness: "started" }, async (client) => await client.bootstrapOwnDeviceVerification({ recoveryKey: opts.recoveryKey?.trim() || undefined, diff --git a/extensions/matrix/src/matrix/client/logging.ts b/extensions/matrix/src/matrix/client/logging.ts index b1384ddfe97..a260aab4619 100644 --- a/extensions/matrix/src/matrix/client/logging.ts +++ b/extensions/matrix/src/matrix/client/logging.ts @@ -1,8 +1,12 @@ -import { ConsoleLogger, LogService } from "../sdk/logger.js"; +import { logger as matrixJsSdkRootLogger } from "matrix-js-sdk/lib/logger.js"; +import { ConsoleLogger, LogService, setMatrixConsoleLogging } from "../sdk/logger.js"; let matrixSdkLoggingConfigured = false; let matrixSdkLogMode: "default" | "quiet" = "default"; const matrixSdkBaseLogger = new ConsoleLogger(); +const matrixSdkSilentMethodFactory = () => () => {}; +let matrixSdkRootMethodFactory: unknown; +let matrixSdkRootLoggerInitialized = false; type MatrixJsSdkLogger = { trace: (...messageOrObject: unknown[]) => void; @@ -40,11 +44,30 @@ export function setMatrixSdkLogMode(mode: "default" | "quiet"): void { applyMatrixSdkLogger(); } +export function setMatrixSdkConsoleLogging(enabled: boolean): void { + setMatrixConsoleLogging(enabled); +} + export function createMatrixJsSdkClientLogger(prefix = "matrix"): MatrixJsSdkLogger { return createMatrixJsSdkLoggerInstance(prefix); } +function applyMatrixJsSdkRootLoggerMode(): void { + const rootLogger = matrixJsSdkRootLogger as { + methodFactory?: unknown; + rebuild?: () => void; + }; + if (!matrixSdkRootLoggerInitialized) { + matrixSdkRootMethodFactory = rootLogger.methodFactory; + matrixSdkRootLoggerInitialized = true; + } + rootLogger.methodFactory = + matrixSdkLogMode === "quiet" ? matrixSdkSilentMethodFactory : matrixSdkRootMethodFactory; + rootLogger.rebuild?.(); +} + function applyMatrixSdkLogger(): void { + applyMatrixJsSdkRootLoggerMode(); if (matrixSdkLogMode === "quiet") { LogService.setLogger({ trace: () => {}, diff --git a/extensions/matrix/src/matrix/sdk.test.ts b/extensions/matrix/src/matrix/sdk.test.ts index 2a0ec77a4ff..b224887168f 100644 --- a/extensions/matrix/src/matrix/sdk.test.ts +++ b/extensions/matrix/src/matrix/sdk.test.ts @@ -1068,13 +1068,13 @@ describe("MatrixClient crypto bootstrapping", () => { encryption: true, recoveryKeyPath: path.join(recoveryDir, "recovery-key.json"), }); - await client.start(); const result = await client.verifyWithRecoveryKey(encoded as string); expect(result.success).toBe(true); expect(result.verified).toBe(true); expect(result.recoveryKeyStored).toBe(true); expect(result.deviceId).toBe("DEVICE123"); + expect(matrixJsClient.startClient).toHaveBeenCalledTimes(1); expect(bootstrapSecretStorage).toHaveBeenCalled(); expect(bootstrapCrossSigning).toHaveBeenCalled(); }); @@ -1265,6 +1265,7 @@ describe("MatrixClient crypto bootstrapping", () => { expect(result.imported).toBe(4); expect(result.total).toBe(10); expect(result.loadedFromSecretStorage).toBe(true); + expect(matrixJsClient.startClient).toHaveBeenCalledTimes(1); expect(loadSessionBackupPrivateKeyFromSecretStorage).toHaveBeenCalledTimes(1); expect(restoreKeyBackup).toHaveBeenCalledTimes(1); }); @@ -1330,6 +1331,7 @@ describe("MatrixClient crypto bootstrapping", () => { expect(result.error).toContain( "Cross-signing bootstrap finished but server keys are still not published", ); + expect(matrixJsClient.startClient).toHaveBeenCalledTimes(1); }); it("reports bootstrap success when own device is verified and keys are published", async () => { diff --git a/extensions/matrix/src/matrix/sdk.ts b/extensions/matrix/src/matrix/sdk.ts index 78eb67f0a7d..d6ac4f3de9a 100644 --- a/extensions/matrix/src/matrix/sdk.ts +++ b/extensions/matrix/src/matrix/sdk.ts @@ -247,6 +247,10 @@ export class MatrixClient { private idbPersistTimer: ReturnType | null = null; async start(): Promise { + await this.startSyncSession({ bootstrapCrypto: true }); + } + + private async startSyncSession(opts: { bootstrapCrypto: boolean }): Promise { if (this.started) { return; } @@ -257,7 +261,7 @@ export class MatrixClient { await this.client.startClient({ initialSyncLimit: this.initialSyncLimit, }); - if (this.autoBootstrapCrypto) { + if (opts.bootstrapCrypto && this.autoBootstrapCrypto) { await this.bootstrapCryptoIfNeeded(); } this.started = true; @@ -281,6 +285,13 @@ export class MatrixClient { } } + private async ensureStartedForCryptoControlPlane(): Promise { + if (this.started) { + return; + } + await this.startSyncSession({ bootstrapCrypto: false }); + } + stop(): void { if (this.idbPersistTimer) { clearInterval(this.idbPersistTimer); @@ -740,6 +751,7 @@ export class MatrixClient { return await fail("Matrix encryption is disabled for this client"); } + await this.ensureStartedForCryptoControlPlane(); const crypto = this.client.getCrypto() as MatrixCryptoBootstrapApi | undefined; if (!crypto) { return await fail("Matrix crypto is not available (start client with encryption enabled)"); @@ -808,7 +820,7 @@ export class MatrixClient { return await fail("Matrix encryption is disabled for this client"); } - await this.initializeCryptoIfNeeded(); + await this.ensureStartedForCryptoControlPlane(); const crypto = this.client.getCrypto() as MatrixCryptoBootstrapApi | undefined; if (!crypto) { return await fail("Matrix crypto is not available (start client with encryption enabled)"); @@ -925,7 +937,7 @@ export class MatrixClient { let bootstrapError: string | undefined; let bootstrapSummary: MatrixCryptoBootstrapResult | null = null; try { - await this.initializeCryptoIfNeeded(); + await this.ensureStartedForCryptoControlPlane(); const crypto = this.client.getCrypto() as MatrixCryptoBootstrapApi | undefined; if (!crypto) { throw new Error("Matrix crypto is not available (start client with encryption enabled)"); diff --git a/extensions/matrix/src/matrix/sdk/logger.ts b/extensions/matrix/src/matrix/sdk/logger.ts index e1f1b3828fa..a0637f17f8d 100644 --- a/extensions/matrix/src/matrix/sdk/logger.ts +++ b/extensions/matrix/src/matrix/sdk/logger.ts @@ -14,7 +14,16 @@ export function noop(): void { // no-op } +let forceConsoleLogging = false; + +export function setMatrixConsoleLogging(enabled: boolean): void { + forceConsoleLogging = enabled; +} + function resolveRuntimeLogger(module: string): RuntimeLogger | null { + if (forceConsoleLogging) { + return null; + } try { return getMatrixRuntime().logging.getChildLogger({ module: `matrix:${module}` }); } catch { diff --git a/src/infra/warning-filter.test.ts b/src/infra/warning-filter.test.ts index 9333d23da0c..30417ddbc3b 100644 --- a/src/infra/warning-filter.test.ts +++ b/src/infra/warning-filter.test.ts @@ -43,6 +43,12 @@ describe("warning filter", () => { message: "SQLite is an experimental feature and might change at any time", }), ).toBe(true); + expect( + shouldIgnoreWarning({ + name: "Warning", + message: "`--localstorage-file` was provided without a valid path", + }), + ).toBe(true); }); it("keeps unknown warnings visible", () => { diff --git a/src/infra/warning-filter.ts b/src/infra/warning-filter.ts index 40863222885..7ec3601c6d1 100644 --- a/src/infra/warning-filter.ts +++ b/src/infra/warning-filter.ts @@ -23,6 +23,9 @@ export function shouldIgnoreWarning(warning: ProcessWarning): boolean { ) { return true; } + if (warning.message?.includes("`--localstorage-file` was provided without a valid path")) { + return true; + } return false; }