mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
Matrix: scope encryption guidance by account
This commit is contained in:
78
extensions/matrix/src/matrix/actions/verification.test.ts
Normal file
78
extensions/matrix/src/matrix/actions/verification.test.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const withResolvedActionClientMock = vi.fn();
|
||||
const loadConfigMock = vi.fn(() => ({
|
||||
channels: {
|
||||
matrix: {},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("../../runtime.js", () => ({
|
||||
getMatrixRuntime: () => ({
|
||||
config: {
|
||||
loadConfig: loadConfigMock,
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("./client.js", () => ({
|
||||
withResolvedActionClient: (...args: unknown[]) => withResolvedActionClientMock(...args),
|
||||
}));
|
||||
|
||||
let listMatrixVerifications: typeof import("./verification.js").listMatrixVerifications;
|
||||
|
||||
describe("matrix verification actions", () => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
vi.clearAllMocks();
|
||||
loadConfigMock.mockReturnValue({
|
||||
channels: {
|
||||
matrix: {},
|
||||
},
|
||||
});
|
||||
({ listMatrixVerifications } = await import("./verification.js"));
|
||||
});
|
||||
|
||||
it("points encryption guidance at the selected Matrix account", async () => {
|
||||
loadConfigMock.mockReturnValue({
|
||||
channels: {
|
||||
matrix: {
|
||||
accounts: {
|
||||
ops: {
|
||||
encryption: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
withResolvedActionClientMock.mockImplementation(async (_opts, run) => {
|
||||
return await run({ crypto: null });
|
||||
});
|
||||
|
||||
await expect(listMatrixVerifications({ accountId: "ops" })).rejects.toThrow(
|
||||
"Matrix encryption is not available (enable channels.matrix.accounts.ops.encryption=true)",
|
||||
);
|
||||
});
|
||||
|
||||
it("uses the resolved default Matrix account when accountId is omitted", async () => {
|
||||
loadConfigMock.mockReturnValue({
|
||||
channels: {
|
||||
matrix: {
|
||||
defaultAccount: "ops",
|
||||
accounts: {
|
||||
ops: {
|
||||
encryption: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
withResolvedActionClientMock.mockImplementation(async (_opts, run) => {
|
||||
return await run({ crypto: null });
|
||||
});
|
||||
|
||||
await expect(listMatrixVerifications()).rejects.toThrow(
|
||||
"Matrix encryption is not available (enable channels.matrix.accounts.ops.encryption=true)",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,11 +1,16 @@
|
||||
import { getMatrixRuntime } from "../../runtime.js";
|
||||
import type { CoreConfig } from "../../types.js";
|
||||
import { formatMatrixEncryptionUnavailableError } from "../encryption-guidance.js";
|
||||
import { withResolvedActionClient } from "./client.js";
|
||||
import type { MatrixActionClientOpts } from "./types.js";
|
||||
|
||||
function requireCrypto(
|
||||
client: import("../sdk.js").MatrixClient,
|
||||
opts: MatrixActionClientOpts,
|
||||
): NonNullable<import("../sdk.js").MatrixClient["crypto"]> {
|
||||
if (!client.crypto) {
|
||||
throw new Error("Matrix encryption is not available (enable channels.matrix.encryption=true)");
|
||||
const cfg = getMatrixRuntime().config.loadConfig() as CoreConfig;
|
||||
throw new Error(formatMatrixEncryptionUnavailableError(cfg, opts.accountId));
|
||||
}
|
||||
return client.crypto;
|
||||
}
|
||||
@@ -22,7 +27,7 @@ export async function listMatrixVerifications(opts: MatrixActionClientOpts = {})
|
||||
return await withResolvedActionClient(
|
||||
{ ...opts, readiness: "started" },
|
||||
async (client) => {
|
||||
const crypto = requireCrypto(client);
|
||||
const crypto = requireCrypto(client, opts);
|
||||
return await crypto.listVerifications();
|
||||
},
|
||||
"persist",
|
||||
@@ -40,7 +45,7 @@ export async function requestMatrixVerification(
|
||||
return await withResolvedActionClient(
|
||||
{ ...params, readiness: "started" },
|
||||
async (client) => {
|
||||
const crypto = requireCrypto(client);
|
||||
const crypto = requireCrypto(client, params);
|
||||
const ownUser = params.ownUser ?? (!params.userId && !params.deviceId && !params.roomId);
|
||||
return await crypto.requestVerification({
|
||||
ownUser,
|
||||
@@ -60,7 +65,7 @@ export async function acceptMatrixVerification(
|
||||
return await withResolvedActionClient(
|
||||
{ ...opts, readiness: "started" },
|
||||
async (client) => {
|
||||
const crypto = requireCrypto(client);
|
||||
const crypto = requireCrypto(client, opts);
|
||||
return await crypto.acceptVerification(resolveVerificationId(requestId));
|
||||
},
|
||||
"persist",
|
||||
@@ -74,7 +79,7 @@ export async function cancelMatrixVerification(
|
||||
return await withResolvedActionClient(
|
||||
{ ...opts, readiness: "started" },
|
||||
async (client) => {
|
||||
const crypto = requireCrypto(client);
|
||||
const crypto = requireCrypto(client, opts);
|
||||
return await crypto.cancelVerification(resolveVerificationId(requestId), {
|
||||
reason: opts.reason?.trim() || undefined,
|
||||
code: opts.code?.trim() || undefined,
|
||||
@@ -91,7 +96,7 @@ export async function startMatrixVerification(
|
||||
return await withResolvedActionClient(
|
||||
{ ...opts, readiness: "started" },
|
||||
async (client) => {
|
||||
const crypto = requireCrypto(client);
|
||||
const crypto = requireCrypto(client, opts);
|
||||
return await crypto.startVerification(resolveVerificationId(requestId), opts.method ?? "sas");
|
||||
},
|
||||
"persist",
|
||||
@@ -105,7 +110,7 @@ export async function generateMatrixVerificationQr(
|
||||
return await withResolvedActionClient(
|
||||
{ ...opts, readiness: "started" },
|
||||
async (client) => {
|
||||
const crypto = requireCrypto(client);
|
||||
const crypto = requireCrypto(client, opts);
|
||||
return await crypto.generateVerificationQr(resolveVerificationId(requestId));
|
||||
},
|
||||
"persist",
|
||||
@@ -120,7 +125,7 @@ export async function scanMatrixVerificationQr(
|
||||
return await withResolvedActionClient(
|
||||
{ ...opts, readiness: "started" },
|
||||
async (client) => {
|
||||
const crypto = requireCrypto(client);
|
||||
const crypto = requireCrypto(client, opts);
|
||||
const payload = qrDataBase64.trim();
|
||||
if (!payload) {
|
||||
throw new Error("Matrix QR data is required");
|
||||
@@ -138,7 +143,7 @@ export async function getMatrixVerificationSas(
|
||||
return await withResolvedActionClient(
|
||||
{ ...opts, readiness: "started" },
|
||||
async (client) => {
|
||||
const crypto = requireCrypto(client);
|
||||
const crypto = requireCrypto(client, opts);
|
||||
return await crypto.getVerificationSas(resolveVerificationId(requestId));
|
||||
},
|
||||
"persist",
|
||||
@@ -152,7 +157,7 @@ export async function confirmMatrixVerificationSas(
|
||||
return await withResolvedActionClient(
|
||||
{ ...opts, readiness: "started" },
|
||||
async (client) => {
|
||||
const crypto = requireCrypto(client);
|
||||
const crypto = requireCrypto(client, opts);
|
||||
return await crypto.confirmVerificationSas(resolveVerificationId(requestId));
|
||||
},
|
||||
"persist",
|
||||
@@ -166,7 +171,7 @@ export async function mismatchMatrixVerificationSas(
|
||||
return await withResolvedActionClient(
|
||||
{ ...opts, readiness: "started" },
|
||||
async (client) => {
|
||||
const crypto = requireCrypto(client);
|
||||
const crypto = requireCrypto(client, opts);
|
||||
return await crypto.mismatchVerificationSas(resolveVerificationId(requestId));
|
||||
},
|
||||
"persist",
|
||||
@@ -180,7 +185,7 @@ export async function confirmMatrixVerificationReciprocateQr(
|
||||
return await withResolvedActionClient(
|
||||
{ ...opts, readiness: "started" },
|
||||
async (client) => {
|
||||
const crypto = requireCrypto(client);
|
||||
const crypto = requireCrypto(client, opts);
|
||||
return await crypto.confirmVerificationReciprocateQr(resolveVerificationId(requestId));
|
||||
},
|
||||
"persist",
|
||||
@@ -193,7 +198,7 @@ export async function getMatrixEncryptionStatus(
|
||||
return await withResolvedActionClient(
|
||||
{ ...opts, readiness: "started" },
|
||||
async (client) => {
|
||||
const crypto = requireCrypto(client);
|
||||
const crypto = requireCrypto(client, opts);
|
||||
const recoveryKey = await crypto.getRecoveryKey();
|
||||
return {
|
||||
encryptionEnabled: true,
|
||||
|
||||
27
extensions/matrix/src/matrix/encryption-guidance.ts
Normal file
27
extensions/matrix/src/matrix/encryption-guidance.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { normalizeOptionalAccountId } from "openclaw/plugin-sdk/account-id";
|
||||
import type { CoreConfig } from "../types.js";
|
||||
import { resolveDefaultMatrixAccountId } from "./accounts.js";
|
||||
import { resolveMatrixConfigPath } from "./config-update.js";
|
||||
|
||||
export function resolveMatrixEncryptionConfigPath(
|
||||
cfg: CoreConfig,
|
||||
accountId?: string | null,
|
||||
): string {
|
||||
const effectiveAccountId =
|
||||
normalizeOptionalAccountId(accountId) ?? resolveDefaultMatrixAccountId(cfg);
|
||||
return `${resolveMatrixConfigPath(cfg, effectiveAccountId)}.encryption`;
|
||||
}
|
||||
|
||||
export function formatMatrixEncryptionUnavailableError(
|
||||
cfg: CoreConfig,
|
||||
accountId?: string | null,
|
||||
): string {
|
||||
return `Matrix encryption is not available (enable ${resolveMatrixEncryptionConfigPath(cfg, accountId)}=true)`;
|
||||
}
|
||||
|
||||
export function formatMatrixEncryptedEventDisabledWarning(
|
||||
cfg: CoreConfig,
|
||||
accountId?: string | null,
|
||||
): string {
|
||||
return `matrix: encrypted event received without encryption enabled; set ${resolveMatrixEncryptionConfigPath(cfg, accountId)}=true and verify the device to decrypt`;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { CoreConfig } from "../../types.js";
|
||||
import type { MatrixAuth } from "../client.js";
|
||||
import type { MatrixClient } from "../sdk.js";
|
||||
import { registerMatrixMonitorEvents } from "./events.js";
|
||||
@@ -14,6 +15,8 @@ function getSentNoticeBody(sendMessage: ReturnType<typeof vi.fn>, index = 0): st
|
||||
}
|
||||
|
||||
function createHarness(params?: {
|
||||
cfg?: CoreConfig;
|
||||
accountId?: string;
|
||||
authEncryption?: boolean;
|
||||
cryptoAvailable?: boolean;
|
||||
verifications?: Array<{
|
||||
@@ -50,9 +53,10 @@ function createHarness(params?: {
|
||||
} as unknown as MatrixClient;
|
||||
|
||||
registerMatrixMonitorEvents({
|
||||
cfg: params?.cfg ?? { channels: { matrix: {} } },
|
||||
client,
|
||||
auth: {
|
||||
accountId: "default",
|
||||
accountId: params?.accountId ?? "default",
|
||||
encryption: params?.authEncryption ?? true,
|
||||
} as MatrixAuth,
|
||||
logVerboseMessage: vi.fn(),
|
||||
@@ -268,6 +272,35 @@ describe("registerMatrixMonitorEvents verification routing", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("uses the active Matrix account path in encrypted-event warnings", () => {
|
||||
const { logger, roomEventListener } = createHarness({
|
||||
accountId: "ops",
|
||||
authEncryption: false,
|
||||
cfg: {
|
||||
channels: {
|
||||
matrix: {
|
||||
accounts: {
|
||||
ops: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
roomEventListener("!room:example.org", {
|
||||
event_id: "$enc1",
|
||||
sender: "@alice:example.org",
|
||||
type: EventType.RoomMessageEncrypted,
|
||||
origin_server_ts: Date.now(),
|
||||
content: {},
|
||||
});
|
||||
|
||||
expect(logger.warn).toHaveBeenCalledWith(
|
||||
"matrix: encrypted event received without encryption enabled; set channels.matrix.accounts.ops.encryption=true and verify the device to decrypt",
|
||||
{ roomId: "!room:example.org" },
|
||||
);
|
||||
});
|
||||
|
||||
it("warns once when crypto bindings are unavailable for encrypted rooms", () => {
|
||||
const { formatNativeDependencyHint, logger, roomEventListener } = createHarness({
|
||||
authEncryption: true,
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import type { PluginRuntime, RuntimeLogger } from "openclaw/plugin-sdk/matrix";
|
||||
import type { CoreConfig } from "../../types.js";
|
||||
import type { MatrixAuth } from "../client.js";
|
||||
import { formatMatrixEncryptedEventDisabledWarning } from "../encryption-guidance.js";
|
||||
import type { MatrixClient } from "../sdk.js";
|
||||
import type { MatrixRawEvent } from "./types.js";
|
||||
import { EventType } from "./types.js";
|
||||
import { createMatrixVerificationEventRouter } from "./verification-events.js";
|
||||
|
||||
export function registerMatrixMonitorEvents(params: {
|
||||
cfg: CoreConfig;
|
||||
client: MatrixClient;
|
||||
auth: MatrixAuth;
|
||||
logVerboseMessage: (message: string) => void;
|
||||
@@ -16,6 +19,7 @@ export function registerMatrixMonitorEvents(params: {
|
||||
onRoomMessage: (roomId: string, event: MatrixRawEvent) => void | Promise<void>;
|
||||
}): void {
|
||||
const {
|
||||
cfg,
|
||||
client,
|
||||
auth,
|
||||
logVerboseMessage,
|
||||
@@ -85,8 +89,7 @@ export function registerMatrixMonitorEvents(params: {
|
||||
);
|
||||
if (auth.encryption !== true && !warnedEncryptedRooms.has(roomId)) {
|
||||
warnedEncryptedRooms.add(roomId);
|
||||
const warning =
|
||||
"matrix: encrypted event received without encryption enabled; set channels.matrix.encryption=true and verify the device to decrypt";
|
||||
const warning = formatMatrixEncryptedEventDisabledWarning(cfg, auth.accountId);
|
||||
logger.warn(warning, { roomId });
|
||||
}
|
||||
if (auth.encryption === true && !client.crypto && !warnedCryptoMissingRooms.has(roomId)) {
|
||||
|
||||
@@ -208,6 +208,7 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
|
||||
);
|
||||
|
||||
registerMatrixMonitorEvents({
|
||||
cfg,
|
||||
client,
|
||||
auth,
|
||||
logVerboseMessage,
|
||||
|
||||
Reference in New Issue
Block a user