mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-16 04:20:44 +00:00
Matrix: fix verification client lifecycle and quiet CLI noise
This commit is contained in:
@@ -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),
|
||||
}));
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -15,11 +15,28 @@ export function ensureNodeRuntime() {
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureActionClientReadiness(
|
||||
client: MatrixActionClient["client"],
|
||||
readiness: MatrixActionClientOpts["readiness"],
|
||||
opts: { createdForOneOff: boolean },
|
||||
): Promise<void> {
|
||||
if (readiness === "started") {
|
||||
await client.start();
|
||||
return;
|
||||
}
|
||||
if (readiness === "prepared" || (!readiness && opts.createdForOneOff)) {
|
||||
await client.prepareForOneOff();
|
||||
}
|
||||
}
|
||||
|
||||
export async function resolveActionClient(
|
||||
opts: MatrixActionClientOpts = {},
|
||||
): Promise<MatrixActionClient> {
|
||||
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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ export type MatrixActionClientOpts = {
|
||||
client?: MatrixClient;
|
||||
timeoutMs?: number;
|
||||
accountId?: string | null;
|
||||
readiness?: "none" | "prepared" | "started";
|
||||
};
|
||||
|
||||
export type MatrixMessageSummary = {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: () => {},
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -247,6 +247,10 @@ export class MatrixClient {
|
||||
private idbPersistTimer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
async start(): Promise<void> {
|
||||
await this.startSyncSession({ bootstrapCrypto: true });
|
||||
}
|
||||
|
||||
private async startSyncSession(opts: { bootstrapCrypto: boolean }): Promise<void> {
|
||||
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<void> {
|
||||
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)");
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user