diff --git a/extensions/matrix/src/matrix/monitor/events.test.ts b/extensions/matrix/src/matrix/monitor/events.test.ts index 54db8ad4f7f..03035f17521 100644 --- a/extensions/matrix/src/matrix/monitor/events.test.ts +++ b/extensions/matrix/src/matrix/monitor/events.test.ts @@ -118,6 +118,56 @@ function createHarness(params?: { } describe("registerMatrixMonitorEvents verification routing", () => { + it("does not repost historical verification completions during startup catch-up", async () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-03-14T13:10:00.000Z")); + try { + const { sendMessage, roomEventListener } = createHarness(); + + roomEventListener("!room:example.org", { + event_id: "$done-old", + sender: "@alice:example.org", + type: "m.key.verification.done", + origin_server_ts: Date.now() - 10 * 60 * 1000, + content: { + "m.relates_to": { event_id: "$req-old" }, + }, + }); + + await vi.runAllTimersAsync(); + expect(sendMessage).not.toHaveBeenCalled(); + } finally { + vi.useRealTimers(); + } + }); + + it("still posts fresh verification completions", async () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-03-14T13:10:00.000Z")); + try { + const { sendMessage, roomEventListener } = createHarness(); + + roomEventListener("!room:example.org", { + event_id: "$done-fresh", + sender: "@alice:example.org", + type: "m.key.verification.done", + origin_server_ts: Date.now(), + content: { + "m.relates_to": { event_id: "$req-fresh" }, + }, + }); + + await vi.waitFor(() => { + expect(sendMessage).toHaveBeenCalledTimes(1); + }); + expect(getSentNoticeBody(sendMessage)).toContain( + "Matrix verification completed with @alice:example.org.", + ); + } finally { + vi.useRealTimers(); + } + }); + it("forwards reaction room events into the shared room handler", async () => { const { onRoomMessage, sendMessage, roomEventListener } = createHarness(); diff --git a/extensions/matrix/src/matrix/monitor/verification-events.ts b/extensions/matrix/src/matrix/monitor/verification-events.ts index cc6a38d120c..b7e951e82d9 100644 --- a/extensions/matrix/src/matrix/monitor/verification-events.ts +++ b/extensions/matrix/src/matrix/monitor/verification-events.ts @@ -11,6 +11,7 @@ import { const MAX_TRACKED_VERIFICATION_EVENTS = 1024; const SAS_NOTICE_RETRY_DELAY_MS = 750; +const VERIFICATION_EVENT_STARTUP_GRACE_MS = 30_000; type MatrixVerificationStage = "request" | "ready" | "start" | "cancel" | "done" | "other"; @@ -306,12 +307,24 @@ export function createMatrixVerificationEventRouter(params: { client: MatrixClient; logVerboseMessage: (message: string) => void; }) { + const routerStartedAtMs = Date.now(); const routedVerificationEvents = new Set(); const routedVerificationSasFingerprints = new Set(); const routedVerificationStageNotices = new Set(); const verificationFlowRooms = new Map(); const verificationUserRooms = new Map(); + function shouldEmitVerificationEventNotice(event: MatrixRawEvent): boolean { + const eventTs = + typeof event.origin_server_ts === "number" && Number.isFinite(event.origin_server_ts) + ? event.origin_server_ts + : null; + if (eventTs === null) { + return true; + } + return eventTs >= routerStartedAtMs - VERIFICATION_EVENT_STARTUP_GRACE_MS; + } + function rememberVerificationRoom(roomId: string, event: MatrixRawEvent, flowId: string | null) { for (const candidate of resolveVerificationFlowCandidates({ event, flowId })) { verificationFlowRooms.set(candidate, roomId); @@ -420,6 +433,12 @@ export function createMatrixVerificationEventRouter(params: { rememberVerificationRoom(roomId, event, signal.flowId); void (async () => { + if (!shouldEmitVerificationEventNotice(event)) { + params.logVerboseMessage( + `matrix: ignoring historical verification event room=${roomId} id=${event.event_id ?? "unknown"} type=${event.type ?? "unknown"}`, + ); + return; + } const flowId = signal.flowId; const sourceEventId = trimMaybeString(event?.event_id); const sourceFingerprint = sourceEventId ?? `${senderId}:${event.type}:${flowId ?? "none"}`;