mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-22 07:20:59 +00:00
Matrix: avoid stale verification SAS reuse
This commit is contained in:
@@ -28,6 +28,9 @@ function createHarness(params?: {
|
||||
otherUserId: string;
|
||||
updatedAt?: string;
|
||||
completed?: boolean;
|
||||
pending?: boolean;
|
||||
phase?: number;
|
||||
phaseName?: string;
|
||||
sas?: {
|
||||
decimal?: [number, number, number];
|
||||
emoji?: Array<[string, string]>;
|
||||
@@ -319,6 +322,117 @@ describe("registerMatrixMonitorEvents verification routing", () => {
|
||||
expect(sasBodies).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("ignores cancelled verification flows when DM fallback resolves SAS notices", async () => {
|
||||
const { sendMessage, roomEventListener } = createHarness({
|
||||
joinedMembersByRoom: {
|
||||
"!dm:example.org": ["@alice:example.org", "@bot:example.org"],
|
||||
},
|
||||
verifications: [
|
||||
{
|
||||
id: "verification-old-cancelled",
|
||||
transactionId: "$old-flow",
|
||||
otherUserId: "@alice:example.org",
|
||||
updatedAt: new Date("2026-02-25T21:42:54.000Z").toISOString(),
|
||||
phaseName: "cancelled",
|
||||
phase: 4,
|
||||
pending: false,
|
||||
sas: {
|
||||
decimal: [1111, 2222, 3333],
|
||||
emoji: [
|
||||
["🚀", "Rocket"],
|
||||
["🦋", "Butterfly"],
|
||||
["📕", "Book"],
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "verification-new-active",
|
||||
transactionId: "$different-flow-id",
|
||||
otherUserId: "@alice:example.org",
|
||||
updatedAt: new Date("2026-02-25T21:43:54.000Z").toISOString(),
|
||||
phaseName: "started",
|
||||
phase: 3,
|
||||
pending: true,
|
||||
sas: {
|
||||
decimal: [6158, 1986, 3513],
|
||||
emoji: [
|
||||
["🎁", "Gift"],
|
||||
["🌍", "Globe"],
|
||||
["🐴", "Horse"],
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
roomEventListener("!dm:example.org", {
|
||||
event_id: "$start-active",
|
||||
sender: "@alice:example.org",
|
||||
type: "m.key.verification.start",
|
||||
origin_server_ts: Date.now(),
|
||||
content: {
|
||||
"m.relates_to": { event_id: "$req-active" },
|
||||
},
|
||||
});
|
||||
|
||||
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 decimal: 6158 1986 3513"))).toBe(true);
|
||||
});
|
||||
const bodies = (sendMessage.mock.calls as unknown[][]).map((call) =>
|
||||
String((call[1] as { body?: string } | undefined)?.body ?? ""),
|
||||
);
|
||||
expect(bodies.some((body) => body.includes("SAS decimal: 1111 2222 3333"))).toBe(false);
|
||||
});
|
||||
|
||||
it("does not emit SAS notices for cancelled verification events", async () => {
|
||||
const { sendMessage, roomEventListener } = createHarness({
|
||||
joinedMembersByRoom: {
|
||||
"!dm:example.org": ["@alice:example.org", "@bot:example.org"],
|
||||
},
|
||||
verifications: [
|
||||
{
|
||||
id: "verification-cancelled",
|
||||
transactionId: "$req-cancelled",
|
||||
otherUserId: "@alice:example.org",
|
||||
updatedAt: new Date("2026-02-25T21:42:54.000Z").toISOString(),
|
||||
phaseName: "cancelled",
|
||||
phase: 4,
|
||||
pending: false,
|
||||
sas: {
|
||||
decimal: [1111, 2222, 3333],
|
||||
emoji: [
|
||||
["🚀", "Rocket"],
|
||||
["🦋", "Butterfly"],
|
||||
["📕", "Book"],
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
roomEventListener("!dm:example.org", {
|
||||
event_id: "$cancelled-1",
|
||||
sender: "@alice:example.org",
|
||||
type: "m.key.verification.cancel",
|
||||
origin_server_ts: Date.now(),
|
||||
content: {
|
||||
code: "m.mismatched_sas",
|
||||
reason: "The SAS did not match.",
|
||||
"m.relates_to": { event_id: "$req-cancelled" },
|
||||
},
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(sendMessage).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
const body = getSentNoticeBody(sendMessage, 0);
|
||||
expect(body).toContain("Matrix verification cancelled by @alice:example.org");
|
||||
expect(body).not.toContain("SAS decimal:");
|
||||
});
|
||||
|
||||
it("warns once when encrypted events arrive without Matrix encryption enabled", () => {
|
||||
const { logger, roomEventListener } = createHarness({
|
||||
authEncryption: false,
|
||||
|
||||
@@ -18,6 +18,9 @@ type MatrixVerificationSummaryLike = {
|
||||
otherUserId: string;
|
||||
updatedAt?: string;
|
||||
completed?: boolean;
|
||||
pending?: boolean;
|
||||
phase?: number;
|
||||
phaseName?: string;
|
||||
sas?: {
|
||||
decimal?: [number, number, number];
|
||||
emoji?: Array<[string, string]>;
|
||||
@@ -162,6 +165,19 @@ function resolveSummaryRecency(summary: MatrixVerificationSummaryLike): number {
|
||||
return Number.isFinite(ts) ? ts : 0;
|
||||
}
|
||||
|
||||
function isActiveVerificationSummary(summary: MatrixVerificationSummaryLike): boolean {
|
||||
if (summary.completed === true) {
|
||||
return false;
|
||||
}
|
||||
if (summary.phaseName === "cancelled" || summary.phaseName === "done") {
|
||||
return false;
|
||||
}
|
||||
if (typeof summary.phase === "number" && summary.phase >= 4) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function resolveVerificationSummaryForSignal(
|
||||
client: MatrixClient,
|
||||
params: {
|
||||
@@ -203,10 +219,10 @@ async function resolveVerificationSummaryForSignal(
|
||||
}
|
||||
|
||||
// Fallback for DM flows where transaction IDs do not match room event IDs consistently.
|
||||
const byUser = list
|
||||
.filter((entry) => entry.otherUserId === params.senderId && entry.completed !== true)
|
||||
.sort((a, b) => resolveSummaryRecency(b) - resolveSummaryRecency(a))[0];
|
||||
return byUser ?? null;
|
||||
const activeByUser = list
|
||||
.filter((entry) => entry.otherUserId === params.senderId && isActiveVerificationSummary(entry))
|
||||
.sort((a, b) => resolveSummaryRecency(b) - resolveSummaryRecency(a));
|
||||
return activeByUser.length === 1 ? (activeByUser[0] ?? null) : null;
|
||||
}
|
||||
|
||||
function trackBounded(set: Set<string>, value: string): boolean {
|
||||
@@ -288,7 +304,10 @@ export function createMatrixVerificationEventRouter(params: {
|
||||
senderId,
|
||||
flowId,
|
||||
}).catch(() => null);
|
||||
const sasNotice = summary ? formatVerificationSasNotice(summary) : null;
|
||||
const sasNotice =
|
||||
summary && isActiveVerificationSummary(summary)
|
||||
? formatVerificationSasNotice(summary)
|
||||
: null;
|
||||
|
||||
const notices: string[] = [];
|
||||
if (stageNotice) {
|
||||
|
||||
Reference in New Issue
Block a user