diff --git a/extensions/matrix/src/matrix/monitor/events.test.ts b/extensions/matrix/src/matrix/monitor/events.test.ts index 4f264d0bde4..c3a73ba9ba8 100644 --- a/extensions/matrix/src/matrix/monitor/events.test.ts +++ b/extensions/matrix/src/matrix/monitor/events.test.ts @@ -65,6 +65,7 @@ function createHarness(params?: { async (roomId: string) => params?.joinedMembersByRoom?.[roomId] ?? ["@bot:example.org", "@alice:example.org"], ), + getJoinedRooms: vi.fn(async () => Object.keys(params?.joinedMembersByRoom ?? {})), ...(params?.cryptoAvailable === false ? {} : { @@ -346,6 +347,50 @@ describe("registerMatrixMonitorEvents verification routing", () => { }); }); + it("posts SAS notices from summary updates using the active strict DM when room mapping is missing", async () => { + const { sendMessage, verificationSummaryListener } = createHarness({ + joinedMembersByRoom: { + "!dm-active:example.org": ["@alice:example.org", "@bot:example.org"], + }, + }); + if (!verificationSummaryListener) { + throw new Error("verification.summary listener was not registered"); + } + + verificationSummaryListener({ + id: "verification-unmapped", + otherUserId: "@alice:example.org", + isSelfVerification: false, + initiatedByMe: false, + phase: 3, + phaseName: "started", + pending: true, + methods: ["m.sas.v1"], + canAccept: false, + hasSas: true, + sas: { + decimal: [4321, 8765, 2109], + emoji: [ + ["🚀", "Rocket"], + ["🦋", "Butterfly"], + ["📕", "Book"], + ], + }, + 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 roomId = (sendMessage.mock.calls[0]?.[0] ?? "") as string; + const body = getSentNoticeBody(sendMessage, 0); + expect(roomId).toBe("!dm-active:example.org"); + expect(body).toContain("SAS decimal: 4321 8765 2109"); + }); + it("retries SAS notice lookup when start arrives before SAS payload is available", async () => { vi.useFakeTimers(); const verifications: Array<{ diff --git a/extensions/matrix/src/matrix/monitor/verification-events.ts b/extensions/matrix/src/matrix/monitor/verification-events.ts index 0147607f0ce..0e5d3573652 100644 --- a/extensions/matrix/src/matrix/monitor/verification-events.ts +++ b/extensions/matrix/src/matrix/monitor/verification-events.ts @@ -1,3 +1,4 @@ +import { inspectMatrixDirectRooms } from "../direct-management.js"; import { isStrictDirectRoom } from "../direct-room.js"; import type { MatrixClient } from "../sdk.js"; import type { MatrixRawEvent } from "./types.js"; @@ -322,18 +323,32 @@ export function createMatrixVerificationEventRouter(params: { } } - function resolveSummaryRoomId(summary: MatrixVerificationSummaryLike): string | null { - return ( + async function resolveSummaryRoomId( + summary: MatrixVerificationSummaryLike, + ): Promise { + const mappedRoomId = trimMaybeString(summary.roomId) ?? trimMaybeString( summary.transactionId ? verificationFlowRooms.get(summary.transactionId) : null, ) ?? - trimMaybeString(verificationFlowRooms.get(summary.id)) - ); + trimMaybeString(verificationFlowRooms.get(summary.id)); + if (mappedRoomId) { + return mappedRoomId; + } + + const remoteUserId = trimMaybeString(summary.otherUserId); + if (!remoteUserId) { + return null; + } + const inspection = await inspectMatrixDirectRooms({ + client: params.client, + remoteUserId, + }).catch(() => null); + return trimMaybeString(inspection?.activeRoomId); } async function routeVerificationSummary(summary: MatrixVerificationSummaryLike): Promise { - const roomId = resolveSummaryRoomId(summary); + const roomId = await resolveSummaryRoomId(summary); if (!roomId || !isActiveVerificationSummary(summary)) { return; }