mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-13 19:10:39 +00:00
Matrix: emit SAS notices from verification state
This commit is contained in:
@@ -2,12 +2,14 @@ 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 type { MatrixVerificationSummary } from "../sdk/verification-manager.js";
|
||||
import { registerMatrixMonitorEvents } from "./events.js";
|
||||
import type { MatrixRawEvent } from "./types.js";
|
||||
import { EventType } from "./types.js";
|
||||
|
||||
type RoomEventListener = (roomId: string, event: MatrixRawEvent) => void;
|
||||
type FailedDecryptListener = (roomId: string, event: MatrixRawEvent, error: Error) => Promise<void>;
|
||||
type VerificationSummaryListener = (summary: MatrixVerificationSummary) => void;
|
||||
|
||||
function getSentNoticeBody(sendMessage: ReturnType<typeof vi.fn>, index = 0): string {
|
||||
const calls = sendMessage.mock.calls as unknown[][];
|
||||
@@ -108,6 +110,9 @@ function createHarness(params?: {
|
||||
failedDecryptListener: listeners.get("room.failed_decryption") as
|
||||
| FailedDecryptListener
|
||||
| undefined,
|
||||
verificationSummaryListener: listeners.get("verification.summary") as
|
||||
| VerificationSummaryListener
|
||||
| undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -242,6 +247,50 @@ describe("registerMatrixMonitorEvents verification routing", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("posts SAS notices directly from verification summary updates", async () => {
|
||||
const { sendMessage, verificationSummaryListener } = createHarness({
|
||||
joinedMembersByRoom: {
|
||||
"!dm:example.org": ["@alice:example.org", "@bot:example.org"],
|
||||
},
|
||||
});
|
||||
if (!verificationSummaryListener) {
|
||||
throw new Error("verification.summary listener was not registered");
|
||||
}
|
||||
|
||||
verificationSummaryListener({
|
||||
id: "verification-direct",
|
||||
roomId: "!dm:example.org",
|
||||
otherUserId: "@alice:example.org",
|
||||
isSelfVerification: false,
|
||||
initiatedByMe: false,
|
||||
phase: 3,
|
||||
phaseName: "started",
|
||||
pending: true,
|
||||
methods: ["m.sas.v1"],
|
||||
canAccept: false,
|
||||
hasSas: true,
|
||||
sas: {
|
||||
decimal: [6158, 1986, 3513],
|
||||
emoji: [
|
||||
["🎁", "Gift"],
|
||||
["🌍", "Globe"],
|
||||
["🐴", "Horse"],
|
||||
],
|
||||
},
|
||||
hasReciprocateQr: false,
|
||||
completed: false,
|
||||
createdAt: new Date("2026-02-25T21:42:54.000Z").toISOString(),
|
||||
updatedAt: new Date("2026-02-25T21:42:55.000Z").toISOString(),
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(sendMessage).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
const body = getSentNoticeBody(sendMessage, 0);
|
||||
expect(body).toContain("Matrix verification SAS with @alice:example.org:");
|
||||
expect(body).toContain("SAS decimal: 6158 1986 3513");
|
||||
});
|
||||
|
||||
it("retries SAS notice lookup when start arrives before SAS payload is available", async () => {
|
||||
vi.useFakeTimers();
|
||||
const verifications: Array<{
|
||||
|
||||
@@ -56,7 +56,7 @@ export function registerMatrixMonitorEvents(params: {
|
||||
formatNativeDependencyHint,
|
||||
onRoomMessage,
|
||||
} = params;
|
||||
const routeVerificationEvent = createMatrixVerificationEventRouter({
|
||||
const { routeVerificationEvent, routeVerificationSummary } = createMatrixVerificationEventRouter({
|
||||
client,
|
||||
logVerboseMessage,
|
||||
});
|
||||
@@ -106,6 +106,10 @@ export function registerMatrixMonitorEvents(params: {
|
||||
},
|
||||
);
|
||||
|
||||
client.on("verification.summary", (summary) => {
|
||||
void routeVerificationSummary(summary);
|
||||
});
|
||||
|
||||
client.on("room.invite", (roomId: string, event: MatrixRawEvent) => {
|
||||
directTracker?.invalidateRoom(roomId);
|
||||
const eventId = event?.event_id ?? "unknown";
|
||||
|
||||
@@ -308,7 +308,40 @@ export function createMatrixVerificationEventRouter(params: {
|
||||
const routedVerificationEvents = new Set<string>();
|
||||
const routedVerificationSasFingerprints = new Set<string>();
|
||||
|
||||
return (roomId: string, event: MatrixRawEvent): boolean => {
|
||||
async function routeVerificationSummary(summary: MatrixVerificationSummaryLike): Promise<void> {
|
||||
const roomId = trimMaybeString(summary.roomId);
|
||||
if (!roomId || !isActiveVerificationSummary(summary)) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!(await isStrictDirectRoom({
|
||||
client: params.client,
|
||||
roomId,
|
||||
remoteUserId: summary.otherUserId,
|
||||
}))
|
||||
) {
|
||||
params.logVerboseMessage(
|
||||
`matrix: ignoring verification summary outside strict DM room=${roomId} sender=${summary.otherUserId}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const sasNotice = formatVerificationSasNotice(summary);
|
||||
if (!sasNotice) {
|
||||
return;
|
||||
}
|
||||
const sasFingerprint = `${summary.id}:${JSON.stringify(summary.sas)}`;
|
||||
if (!trackBounded(routedVerificationSasFingerprints, sasFingerprint)) {
|
||||
return;
|
||||
}
|
||||
await sendVerificationNotice({
|
||||
client: params.client,
|
||||
roomId,
|
||||
body: sasNotice,
|
||||
logVerboseMessage: params.logVerboseMessage,
|
||||
});
|
||||
}
|
||||
|
||||
function routeVerificationEvent(roomId: string, event: MatrixRawEvent): boolean {
|
||||
const senderId = trimMaybeString(event?.sender);
|
||||
if (!senderId) {
|
||||
return false;
|
||||
@@ -373,5 +406,10 @@ export function createMatrixVerificationEventRouter(params: {
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return {
|
||||
routeVerificationEvent,
|
||||
routeVerificationSummary,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -29,7 +29,10 @@ import type {
|
||||
MatrixRawEvent,
|
||||
MessageEventContent,
|
||||
} from "./sdk/types.js";
|
||||
import { MatrixVerificationManager } from "./sdk/verification-manager.js";
|
||||
import {
|
||||
MatrixVerificationManager,
|
||||
type MatrixVerificationSummary,
|
||||
} from "./sdk/verification-manager.js";
|
||||
import { isMatrixDeviceOwnerVerified } from "./sdk/verification-status.js";
|
||||
|
||||
export { ConsoleLogger, LogService };
|
||||
@@ -247,6 +250,9 @@ export class MatrixClient {
|
||||
recoveryKeyStore: this.recoveryKeyStore,
|
||||
decryptBridge: this.decryptBridge,
|
||||
});
|
||||
this.verificationManager.onSummaryChanged((summary: MatrixVerificationSummary) => {
|
||||
this.emitter.emit("verification.summary", summary);
|
||||
});
|
||||
|
||||
if (this.encryptionEnabled) {
|
||||
this.crypto = createMatrixCryptoFacade({
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { MatrixVerificationRequestLike } from "./verification-manager.js";
|
||||
import type {
|
||||
MatrixVerificationRequestLike,
|
||||
MatrixVerificationSummary,
|
||||
} from "./verification-manager.js";
|
||||
|
||||
export type MatrixRawEvent = {
|
||||
event_id: string;
|
||||
@@ -28,6 +31,7 @@ export type MatrixClientEventMap = {
|
||||
"room.failed_decryption": [roomId: string, event: MatrixRawEvent, error: Error];
|
||||
"room.invite": [roomId: string, event: MatrixRawEvent];
|
||||
"room.join": [roomId: string, event: MatrixRawEvent];
|
||||
"verification.summary": [summary: MatrixVerificationSummary];
|
||||
};
|
||||
|
||||
export type EncryptedFile = {
|
||||
|
||||
@@ -209,6 +209,52 @@ describe("MatrixVerificationManager", () => {
|
||||
expect(manager.getVerificationSas(tracked.id).decimal).toEqual([6158, 1986, 3513]);
|
||||
});
|
||||
|
||||
it("emits summary updates when SAS becomes available", async () => {
|
||||
const verify = vi.fn(async () => {});
|
||||
const verifier = new MockVerifier(
|
||||
{
|
||||
sas: {
|
||||
decimal: [6158, 1986, 3513],
|
||||
emoji: [
|
||||
["gift", "Gift"],
|
||||
["globe", "Globe"],
|
||||
["horse", "Horse"],
|
||||
],
|
||||
},
|
||||
confirm: vi.fn(async () => {}),
|
||||
mismatch: vi.fn(),
|
||||
cancel: vi.fn(),
|
||||
},
|
||||
null,
|
||||
verify,
|
||||
);
|
||||
const request = new MockVerificationRequest({
|
||||
transactionId: "txn-summary-listener",
|
||||
roomId: "!dm:example.org",
|
||||
verifier: undefined,
|
||||
});
|
||||
const manager = new MatrixVerificationManager();
|
||||
const summaries: ReturnType<typeof manager.listVerifications> = [];
|
||||
manager.onSummaryChanged((summary) => {
|
||||
summaries.push(summary);
|
||||
});
|
||||
|
||||
manager.trackVerificationRequest(request);
|
||||
request.verifier = verifier;
|
||||
request.emit(VerificationRequestEvent.Change);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(
|
||||
summaries.some(
|
||||
(summary) =>
|
||||
summary.transactionId === "txn-summary-listener" &&
|
||||
summary.roomId === "!dm:example.org" &&
|
||||
summary.hasSas,
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it("auto-starts inbound SAS when request becomes ready without a verifier", async () => {
|
||||
const verify = vi.fn(async () => {});
|
||||
const verifier = new MockVerifier(
|
||||
|
||||
@@ -33,6 +33,8 @@ export type MatrixVerificationSummary = {
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
type MatrixVerificationSummaryListener = (summary: MatrixVerificationSummary) => void;
|
||||
|
||||
export type MatrixShowSasCallbacks = {
|
||||
sas: {
|
||||
decimal?: [number, number, number];
|
||||
@@ -116,6 +118,7 @@ export class MatrixVerificationManager {
|
||||
private verificationSessionCounter = 0;
|
||||
private readonly trackedVerificationRequests = new WeakSet<object>();
|
||||
private readonly trackedVerificationVerifiers = new WeakSet<object>();
|
||||
private readonly summaryListeners = new Set<MatrixVerificationSummaryListener>();
|
||||
|
||||
private readRequestValue<T>(
|
||||
request: MatrixVerificationRequestLike,
|
||||
@@ -173,8 +176,16 @@ export class MatrixVerificationManager {
|
||||
}
|
||||
}
|
||||
|
||||
private emitVerificationSummary(session: MatrixVerificationSession): void {
|
||||
const summary = this.buildVerificationSummary(session);
|
||||
for (const listener of this.summaryListeners) {
|
||||
listener(summary);
|
||||
}
|
||||
}
|
||||
|
||||
private touchVerificationSession(session: MatrixVerificationSession): void {
|
||||
session.updatedAtMs = Date.now();
|
||||
this.emitVerificationSummary(session);
|
||||
}
|
||||
|
||||
private clearSasAutoConfirmTimer(session: MatrixVerificationSession): void {
|
||||
@@ -400,6 +411,13 @@ export class MatrixVerificationManager {
|
||||
});
|
||||
}
|
||||
|
||||
onSummaryChanged(listener: MatrixVerificationSummaryListener): () => void {
|
||||
this.summaryListeners.add(listener);
|
||||
return () => {
|
||||
this.summaryListeners.delete(listener);
|
||||
};
|
||||
}
|
||||
|
||||
trackVerificationRequest(request: MatrixVerificationRequestLike): MatrixVerificationSummary {
|
||||
this.pruneVerificationSessions(Date.now());
|
||||
const txId = this.readRequestValue(request, () => request.transactionId?.trim(), "");
|
||||
@@ -441,6 +459,7 @@ export class MatrixVerificationManager {
|
||||
this.attachVerifierToVerificationSession(session, verifier);
|
||||
}
|
||||
this.maybeAutoStartInboundSas(session);
|
||||
this.emitVerificationSummary(session);
|
||||
return this.buildVerificationSummary(session);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user