Matrix: retry SAS notice after verification start

This commit is contained in:
Gustavo Madeira Santana 2026-03-13 02:17:55 +00:00
parent a1604a668a
commit 8fd04a9075
No known key found for this signature in database
2 changed files with 97 additions and 6 deletions

View File

@ -241,6 +241,68 @@ describe("registerMatrixMonitorEvents verification routing", () => {
});
});
it("retries SAS notice lookup when start arrives before SAS payload is available", async () => {
vi.useFakeTimers();
const verifications: Array<{
id: string;
transactionId?: string;
otherUserId: string;
updatedAt?: string;
sas?: {
decimal?: [number, number, number];
emoji?: Array<[string, string]>;
};
}> = [
{
id: "verification-race",
transactionId: "$req-race",
updatedAt: new Date("2026-02-25T21:42:54.000Z").toISOString(),
otherUserId: "@alice:example.org",
},
];
const { sendMessage, roomEventListener } = createHarness({
joinedMembersByRoom: {
"!dm:example.org": ["@alice:example.org", "@bot:example.org"],
},
verifications,
});
try {
roomEventListener("!dm:example.org", {
event_id: "$start-race",
sender: "@alice:example.org",
type: "m.key.verification.start",
origin_server_ts: Date.now(),
content: {
"m.relates_to": { event_id: "$req-race" },
},
});
await vi.advanceTimersByTimeAsync(500);
verifications[0] = {
...verifications[0]!,
sas: {
decimal: [1234, 5678, 9012],
emoji: [
["🚀", "Rocket"],
["🦋", "Butterfly"],
["📕", "Book"],
],
},
};
await vi.advanceTimersByTimeAsync(500);
await vi.waitFor(() => {
const bodies = (sendMessage.mock.calls as unknown[][]).map((call) =>
String((call[1] as { body?: string } | undefined)?.body ?? ""),
);
expect(bodies.some((body) => body.includes("SAS emoji:"))).toBe(true);
});
} finally {
vi.useRealTimers();
}
});
it("ignores verification notices in unrelated non-DM rooms", async () => {
const { sendMessage, roomEventListener } = createHarness({
joinedMembersByRoom: {

View File

@ -9,6 +9,7 @@ import {
} from "./verification-utils.js";
const MAX_TRACKED_VERIFICATION_EVENTS = 1024;
const SAS_NOTICE_RETRY_DELAY_MS = 750;
type MatrixVerificationStage = "request" | "ready" | "start" | "cancel" | "done" | "other";
@ -225,6 +226,37 @@ async function resolveVerificationSummaryForSignal(
return activeByUser.length === 1 ? (activeByUser[0] ?? null) : null;
}
async function resolveVerificationSasNoticeForSignal(
client: MatrixClient,
params: {
roomId: string;
event: MatrixRawEvent;
senderId: string;
flowId: string | null;
stage: MatrixVerificationStage;
},
): Promise<{ summary: MatrixVerificationSummaryLike | null; sasNotice: string | null }> {
const summary = await resolveVerificationSummaryForSignal(client, params);
const immediateNotice =
summary && isActiveVerificationSummary(summary) ? formatVerificationSasNotice(summary) : null;
if (immediateNotice || (params.stage !== "ready" && params.stage !== "start")) {
return {
summary,
sasNotice: immediateNotice,
};
}
await new Promise((resolve) => setTimeout(resolve, SAS_NOTICE_RETRY_DELAY_MS));
const retriedSummary = await resolveVerificationSummaryForSignal(client, params);
return {
summary: retriedSummary,
sasNotice:
retriedSummary && isActiveVerificationSummary(retriedSummary)
? formatVerificationSasNotice(retriedSummary)
: null,
};
}
function trackBounded(set: Set<string>, value: string): boolean {
if (!value || set.has(value)) {
return false;
@ -298,16 +330,13 @@ export function createMatrixVerificationEventRouter(params: {
}
const stageNotice = formatVerificationStageNotice({ stage: signal.stage, senderId, event });
const summary = await resolveVerificationSummaryForSignal(params.client, {
const { summary, sasNotice } = await resolveVerificationSasNoticeForSignal(params.client, {
roomId,
event,
senderId,
flowId,
}).catch(() => null);
const sasNotice =
summary && isActiveVerificationSummary(summary)
? formatVerificationSasNotice(summary)
: null;
stage: signal.stage,
}).catch(() => ({ summary: null, sasNotice: null }));
const notices: string[] = [];
if (stageNotice) {