mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 13:10:43 +00:00
fix: rehydrate Matrix DM verifications
This commit is contained in:
@@ -388,6 +388,53 @@ describe("matrix CLI verification commands", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("prints DM lookup details in Matrix verification follow-up commands", async () => {
|
||||
requestMatrixVerificationMock.mockResolvedValue(
|
||||
mockMatrixVerificationSummary({
|
||||
id: "dm-verify-1",
|
||||
transactionId: "txn-dm",
|
||||
roomId: "!room-'$(x):example.org",
|
||||
otherUserId: "@alice:example.org",
|
||||
isSelfVerification: false,
|
||||
hasSas: false,
|
||||
sas: undefined,
|
||||
}),
|
||||
);
|
||||
const program = buildProgram();
|
||||
|
||||
await program.parseAsync(
|
||||
[
|
||||
"matrix",
|
||||
"verify",
|
||||
"request",
|
||||
"--user-id",
|
||||
"@alice:example.org",
|
||||
"--room-id",
|
||||
"!room-'$(x):example.org",
|
||||
],
|
||||
{ from: "user" },
|
||||
);
|
||||
|
||||
expect(requestMatrixVerificationMock).toHaveBeenCalledWith({
|
||||
accountId: "default",
|
||||
cfg: {},
|
||||
ownUser: undefined,
|
||||
userId: "@alice:example.org",
|
||||
deviceId: undefined,
|
||||
roomId: "!room-'$(x):example.org",
|
||||
});
|
||||
expect(consoleLogMock).toHaveBeenCalledWith("Room id: !room-'$(x):example.org");
|
||||
expect(consoleLogMock).toHaveBeenCalledWith(
|
||||
"- Then run openclaw matrix verify start txn-dm --user-id @alice:example.org --room-id '!room-'\\''$(x):example.org' to start SAS verification.",
|
||||
);
|
||||
expect(consoleLogMock).toHaveBeenCalledWith(
|
||||
"- Run openclaw matrix verify sas txn-dm --user-id @alice:example.org --room-id '!room-'\\''$(x):example.org' to display the SAS emoji or decimals.",
|
||||
);
|
||||
expect(consoleLogMock).toHaveBeenCalledWith(
|
||||
"- When the SAS matches, run openclaw matrix verify confirm-sas txn-dm --user-id @alice:example.org --room-id '!room-'\\''$(x):example.org'.",
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects ambiguous Matrix verification request targets", async () => {
|
||||
const program = buildProgram();
|
||||
|
||||
@@ -529,6 +576,45 @@ describe("matrix CLI verification commands", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("passes DM lookup details through Matrix verification follow-up commands", async () => {
|
||||
startMatrixVerificationMock.mockResolvedValue(
|
||||
mockMatrixVerificationSummary({
|
||||
id: "dm-verify-1",
|
||||
transactionId: "txn-dm",
|
||||
roomId: "!dm:example.org",
|
||||
otherUserId: "@alice:example.org",
|
||||
}),
|
||||
);
|
||||
const program = buildProgram();
|
||||
|
||||
await program.parseAsync(
|
||||
[
|
||||
"matrix",
|
||||
"verify",
|
||||
"start",
|
||||
"txn-dm",
|
||||
"--user-id",
|
||||
"@alice:example.org",
|
||||
"--room-id",
|
||||
"!dm:example.org",
|
||||
"--account",
|
||||
"ops",
|
||||
],
|
||||
{ from: "user" },
|
||||
);
|
||||
|
||||
expect(startMatrixVerificationMock).toHaveBeenCalledWith("txn-dm", {
|
||||
accountId: "ops",
|
||||
cfg: {},
|
||||
method: "sas",
|
||||
verificationDmUserId: "@alice:example.org",
|
||||
verificationDmRoomId: "!dm:example.org",
|
||||
});
|
||||
expect(consoleLogMock).toHaveBeenCalledWith(
|
||||
"- If they match, run openclaw matrix verify confirm-sas txn-dm --user-id @alice:example.org --room-id '!dm:example.org' --account ops.",
|
||||
);
|
||||
});
|
||||
|
||||
it("prints stable transaction ids in follow-up commands after accepting verification", async () => {
|
||||
acceptMatrixVerificationMock.mockResolvedValue(
|
||||
mockMatrixVerificationSummary({
|
||||
|
||||
@@ -544,6 +544,8 @@ type MatrixCliVerificationStatus = {
|
||||
|
||||
type MatrixCliVerificationCommandOptions = {
|
||||
account?: string;
|
||||
userId?: string;
|
||||
roomId?: string;
|
||||
verbose?: boolean;
|
||||
json?: boolean;
|
||||
};
|
||||
@@ -557,6 +559,7 @@ type MatrixCliSelfVerificationCommandOptions = {
|
||||
type MatrixCliVerificationSummary = {
|
||||
id: string;
|
||||
transactionId?: string;
|
||||
roomId?: string;
|
||||
otherUserId: string;
|
||||
otherDeviceId?: string;
|
||||
isSelfVerification: boolean;
|
||||
@@ -789,6 +792,9 @@ function printMatrixVerificationSummary(summary: MatrixCliVerificationSummary):
|
||||
if (summary.transactionId) {
|
||||
console.log(`Transaction id: ${sanitizeMatrixCliText(summary.transactionId)}`);
|
||||
}
|
||||
if (summary.roomId) {
|
||||
console.log(`Room id: ${sanitizeMatrixCliText(summary.roomId)}`);
|
||||
}
|
||||
console.log(`Other user: ${sanitizeMatrixCliText(summary.otherUserId)}`);
|
||||
console.log(`Other device: ${sanitizeMatrixCliText(summary.otherDeviceId ?? "unknown")}`);
|
||||
console.log(`Self-verification: ${summary.isSelfVerification ? "yes" : "no"}`);
|
||||
@@ -837,11 +843,87 @@ function printMatrixVerificationSas(sas: MatrixCliVerificationSas): void {
|
||||
}
|
||||
}
|
||||
|
||||
function printMatrixVerificationSasGuidance(requestId: string, accountId?: string): void {
|
||||
function matrixCliVerificationDmLookupOptions(options: MatrixCliVerificationCommandOptions): {
|
||||
verificationDmRoomId?: string;
|
||||
verificationDmUserId?: string;
|
||||
} {
|
||||
const lookup: {
|
||||
verificationDmRoomId?: string;
|
||||
verificationDmUserId?: string;
|
||||
} = {};
|
||||
if (options.roomId !== undefined) {
|
||||
lookup.verificationDmRoomId = options.roomId;
|
||||
}
|
||||
if (options.userId !== undefined) {
|
||||
lookup.verificationDmUserId = options.userId;
|
||||
}
|
||||
return lookup;
|
||||
}
|
||||
|
||||
function formatMatrixVerificationDmFollowupParts(params: {
|
||||
roomId?: string;
|
||||
userId?: string;
|
||||
}): string[] {
|
||||
if (!params.roomId || !params.userId) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
"--user-id",
|
||||
sanitizeMatrixCliText(params.userId),
|
||||
"--room-id",
|
||||
sanitizeMatrixCliText(params.roomId),
|
||||
];
|
||||
}
|
||||
|
||||
function formatMatrixVerificationSummaryDmFollowupParts(
|
||||
summary: MatrixCliVerificationSummary,
|
||||
): string[] {
|
||||
return formatMatrixVerificationDmFollowupParts({
|
||||
roomId: summary.roomId,
|
||||
userId: summary.otherUserId,
|
||||
});
|
||||
}
|
||||
|
||||
function formatMatrixVerificationOptionsDmFollowupParts(
|
||||
options: MatrixCliVerificationCommandOptions,
|
||||
): string[] {
|
||||
return formatMatrixVerificationDmFollowupParts({
|
||||
roomId: options.roomId,
|
||||
userId: options.userId,
|
||||
});
|
||||
}
|
||||
|
||||
function formatMatrixVerificationPreferredDmFollowupParts(
|
||||
summary: MatrixCliVerificationSummary,
|
||||
options: MatrixCliVerificationCommandOptions,
|
||||
): string[] {
|
||||
const summaryParts = formatMatrixVerificationSummaryDmFollowupParts(summary);
|
||||
return summaryParts.length
|
||||
? summaryParts
|
||||
: formatMatrixVerificationOptionsDmFollowupParts(options);
|
||||
}
|
||||
|
||||
function formatMatrixVerificationFollowupCommand(params: {
|
||||
action: string;
|
||||
requestId: string;
|
||||
accountId?: string;
|
||||
dmParts?: string[];
|
||||
}): string {
|
||||
return formatMatrixCliCommandParts(
|
||||
["verify", params.action, params.requestId, ...(params.dmParts ?? [])],
|
||||
params.accountId,
|
||||
);
|
||||
}
|
||||
|
||||
function printMatrixVerificationSasGuidance(
|
||||
requestId: string,
|
||||
accountId?: string,
|
||||
dmParts: string[] = [],
|
||||
): void {
|
||||
printGuidance([
|
||||
`Compare the emoji or decimals with the other Matrix client.`,
|
||||
`If they match, run ${formatMatrixCliCommandParts(["verify", "confirm-sas", requestId], accountId)}.`,
|
||||
`If they do not match, run ${formatMatrixCliCommandParts(["verify", "mismatch-sas", requestId], accountId)}.`,
|
||||
`If they match, run ${formatMatrixVerificationFollowupCommand({ action: "confirm-sas", requestId, accountId, dmParts })}.`,
|
||||
`If they do not match, run ${formatMatrixVerificationFollowupCommand({ action: "mismatch-sas", requestId, accountId, dmParts })}.`,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -863,12 +945,17 @@ async function promptMatrixVerificationSasMatch(): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
function printMatrixVerificationRequestGuidance(requestId: string, accountId?: string): void {
|
||||
function printMatrixVerificationRequestGuidance(
|
||||
summary: MatrixCliVerificationSummary,
|
||||
accountId?: string,
|
||||
): void {
|
||||
const requestId = formatMatrixVerificationCommandId(summary);
|
||||
const dmParts = formatMatrixVerificationSummaryDmFollowupParts(summary);
|
||||
printGuidance([
|
||||
`Accept the verification request in another Matrix client for this account.`,
|
||||
`Then run ${formatMatrixCliCommandParts(["verify", "start", requestId], accountId)} to start SAS verification.`,
|
||||
`Run ${formatMatrixCliCommandParts(["verify", "sas", requestId], accountId)} to display the SAS emoji or decimals.`,
|
||||
`When the SAS matches, run ${formatMatrixCliCommandParts(["verify", "confirm-sas", requestId], accountId)}.`,
|
||||
`Then run ${formatMatrixVerificationFollowupCommand({ action: "start", requestId, accountId, dmParts })} to start SAS verification.`,
|
||||
`Run ${formatMatrixVerificationFollowupCommand({ action: "sas", requestId, accountId, dmParts })} to display the SAS emoji or decimals.`,
|
||||
`When the SAS matches, run ${formatMatrixVerificationFollowupCommand({ action: "confirm-sas", requestId, accountId, dmParts })}.`,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -1357,10 +1444,7 @@ export function registerMatrixCli(params: { program: Command }): void {
|
||||
onText: (summary) => {
|
||||
printAccountLabel(accountId);
|
||||
printMatrixVerificationSummary(summary);
|
||||
printMatrixVerificationRequestGuidance(
|
||||
formatMatrixVerificationCommandId(summary),
|
||||
accountId,
|
||||
);
|
||||
printMatrixVerificationRequestGuidance(summary, accountId);
|
||||
},
|
||||
errorPrefix: "Verification request failed",
|
||||
});
|
||||
@@ -1371,15 +1455,24 @@ export function registerMatrixCli(params: { program: Command }): void {
|
||||
.command("accept <id>")
|
||||
.description("Accept an inbound Matrix verification request")
|
||||
.option("--account <id>", "Account ID (for multi-account setups)")
|
||||
.option("--user-id <id>", "Matrix user ID for DM verification follow-up")
|
||||
.option("--room-id <id>", "Matrix direct-message room ID for verification follow-up")
|
||||
.option("--verbose", "Show detailed diagnostics")
|
||||
.option("--json", "Output as JSON")
|
||||
.action(async (id: string, options: MatrixCliVerificationCommandOptions) => {
|
||||
await runMatrixCliVerificationSummaryCommand({
|
||||
options,
|
||||
run: async (accountId, cfg) => await acceptMatrixVerification(id, { accountId, cfg }),
|
||||
run: async (accountId, cfg) =>
|
||||
await acceptMatrixVerification(id, {
|
||||
accountId,
|
||||
cfg,
|
||||
...matrixCliVerificationDmLookupOptions(options),
|
||||
}),
|
||||
afterText: (summary, accountId) => {
|
||||
const requestId = formatMatrixVerificationCommandId(summary);
|
||||
const dmParts = formatMatrixVerificationPreferredDmFollowupParts(summary, options);
|
||||
printGuidance([
|
||||
`Run ${formatMatrixCliCommandParts(["verify", "start", formatMatrixVerificationCommandId(summary)], accountId)} to start SAS verification.`,
|
||||
`Run ${formatMatrixVerificationFollowupCommand({ action: "start", requestId, accountId, dmParts })} to start SAS verification.`,
|
||||
]);
|
||||
},
|
||||
errorPrefix: "Verification accept failed",
|
||||
@@ -1390,15 +1483,26 @@ export function registerMatrixCli(params: { program: Command }): void {
|
||||
.command("start <id>")
|
||||
.description("Start SAS verification for a Matrix verification request")
|
||||
.option("--account <id>", "Account ID (for multi-account setups)")
|
||||
.option("--user-id <id>", "Matrix user ID for DM verification follow-up")
|
||||
.option("--room-id <id>", "Matrix direct-message room ID for verification follow-up")
|
||||
.option("--verbose", "Show detailed diagnostics")
|
||||
.option("--json", "Output as JSON")
|
||||
.action(async (id: string, options: MatrixCliVerificationCommandOptions) => {
|
||||
await runMatrixCliVerificationSummaryCommand({
|
||||
options,
|
||||
run: async (accountId, cfg) =>
|
||||
await startMatrixVerification(id, { accountId, cfg, method: "sas" }),
|
||||
await startMatrixVerification(id, {
|
||||
accountId,
|
||||
cfg,
|
||||
method: "sas",
|
||||
...matrixCliVerificationDmLookupOptions(options),
|
||||
}),
|
||||
afterText: (summary, accountId) =>
|
||||
printMatrixVerificationSasGuidance(formatMatrixVerificationCommandId(summary), accountId),
|
||||
printMatrixVerificationSasGuidance(
|
||||
formatMatrixVerificationCommandId(summary),
|
||||
accountId,
|
||||
formatMatrixVerificationPreferredDmFollowupParts(summary, options),
|
||||
),
|
||||
errorPrefix: "Verification start failed",
|
||||
});
|
||||
});
|
||||
@@ -1407,6 +1511,8 @@ export function registerMatrixCli(params: { program: Command }): void {
|
||||
.command("sas <id>")
|
||||
.description("Show SAS emoji or decimals for a Matrix verification request")
|
||||
.option("--account <id>", "Account ID (for multi-account setups)")
|
||||
.option("--user-id <id>", "Matrix user ID for DM verification follow-up")
|
||||
.option("--room-id <id>", "Matrix direct-message room ID for verification follow-up")
|
||||
.option("--verbose", "Show detailed diagnostics")
|
||||
.option("--json", "Output as JSON")
|
||||
.action(async (id: string, options: MatrixCliVerificationCommandOptions) => {
|
||||
@@ -1414,12 +1520,22 @@ export function registerMatrixCli(params: { program: Command }): void {
|
||||
await runMatrixCliCommand({
|
||||
verbose: options.verbose === true,
|
||||
json: options.json === true,
|
||||
run: async () => await getMatrixVerificationSas(id, { accountId, cfg }),
|
||||
run: async () =>
|
||||
await getMatrixVerificationSas(id, {
|
||||
accountId,
|
||||
cfg,
|
||||
...matrixCliVerificationDmLookupOptions(options),
|
||||
}),
|
||||
onText: (sas) => {
|
||||
const requestId = formatMatrixCliText(id);
|
||||
printAccountLabel(accountId);
|
||||
console.log(`Verification id: ${formatMatrixCliText(id)}`);
|
||||
console.log(`Verification id: ${requestId}`);
|
||||
printMatrixVerificationSas(sas);
|
||||
printMatrixVerificationSasGuidance(id, accountId);
|
||||
printMatrixVerificationSasGuidance(
|
||||
requestId,
|
||||
accountId,
|
||||
formatMatrixVerificationOptionsDmFollowupParts(options),
|
||||
);
|
||||
},
|
||||
errorPrefix: "Verification SAS lookup failed",
|
||||
});
|
||||
@@ -1429,12 +1545,19 @@ export function registerMatrixCli(params: { program: Command }): void {
|
||||
.command("confirm-sas <id>")
|
||||
.description("Confirm matching SAS emoji or decimals for a Matrix verification request")
|
||||
.option("--account <id>", "Account ID (for multi-account setups)")
|
||||
.option("--user-id <id>", "Matrix user ID for DM verification follow-up")
|
||||
.option("--room-id <id>", "Matrix direct-message room ID for verification follow-up")
|
||||
.option("--verbose", "Show detailed diagnostics")
|
||||
.option("--json", "Output as JSON")
|
||||
.action(async (id: string, options: MatrixCliVerificationCommandOptions) => {
|
||||
await runMatrixCliVerificationSummaryCommand({
|
||||
options,
|
||||
run: async (accountId, cfg) => await confirmMatrixVerificationSas(id, { accountId, cfg }),
|
||||
run: async (accountId, cfg) =>
|
||||
await confirmMatrixVerificationSas(id, {
|
||||
accountId,
|
||||
cfg,
|
||||
...matrixCliVerificationDmLookupOptions(options),
|
||||
}),
|
||||
errorPrefix: "Verification SAS confirm failed",
|
||||
});
|
||||
});
|
||||
@@ -1443,12 +1566,19 @@ export function registerMatrixCli(params: { program: Command }): void {
|
||||
.command("mismatch-sas <id>")
|
||||
.description("Reject a Matrix SAS verification when the emoji or decimals do not match")
|
||||
.option("--account <id>", "Account ID (for multi-account setups)")
|
||||
.option("--user-id <id>", "Matrix user ID for DM verification follow-up")
|
||||
.option("--room-id <id>", "Matrix direct-message room ID for verification follow-up")
|
||||
.option("--verbose", "Show detailed diagnostics")
|
||||
.option("--json", "Output as JSON")
|
||||
.action(async (id: string, options: MatrixCliVerificationCommandOptions) => {
|
||||
await runMatrixCliVerificationSummaryCommand({
|
||||
options,
|
||||
run: async (accountId, cfg) => await mismatchMatrixVerificationSas(id, { accountId, cfg }),
|
||||
run: async (accountId, cfg) =>
|
||||
await mismatchMatrixVerificationSas(id, {
|
||||
accountId,
|
||||
cfg,
|
||||
...matrixCliVerificationDmLookupOptions(options),
|
||||
}),
|
||||
errorPrefix: "Verification SAS mismatch failed",
|
||||
});
|
||||
});
|
||||
@@ -1457,6 +1587,8 @@ export function registerMatrixCli(params: { program: Command }): void {
|
||||
.command("cancel <id>")
|
||||
.description("Cancel a Matrix verification request")
|
||||
.option("--account <id>", "Account ID (for multi-account setups)")
|
||||
.option("--user-id <id>", "Matrix user ID for DM verification follow-up")
|
||||
.option("--room-id <id>", "Matrix direct-message room ID for verification follow-up")
|
||||
.option("--reason <text>", "Cancellation reason")
|
||||
.option("--code <code>", "Matrix cancellation code")
|
||||
.option("--verbose", "Show detailed diagnostics")
|
||||
@@ -1477,6 +1609,7 @@ export function registerMatrixCli(params: { program: Command }): void {
|
||||
cfg,
|
||||
reason: options.reason,
|
||||
code: options.code,
|
||||
...matrixCliVerificationDmLookupOptions(options),
|
||||
}),
|
||||
errorPrefix: "Verification cancel failed",
|
||||
});
|
||||
|
||||
@@ -36,6 +36,7 @@ let getMatrixEncryptionStatus: typeof import("./verification.js").getMatrixEncry
|
||||
let getMatrixRoomKeyBackupStatus: typeof import("./verification.js").getMatrixRoomKeyBackupStatus;
|
||||
let getMatrixVerificationStatus: typeof import("./verification.js").getMatrixVerificationStatus;
|
||||
let runMatrixSelfVerification: typeof import("./verification.js").runMatrixSelfVerification;
|
||||
let startMatrixVerification: typeof import("./verification.js").startMatrixVerification;
|
||||
|
||||
describe("matrix verification actions", () => {
|
||||
beforeAll(async () => {
|
||||
@@ -45,6 +46,7 @@ describe("matrix verification actions", () => {
|
||||
getMatrixVerificationStatus,
|
||||
listMatrixVerifications,
|
||||
runMatrixSelfVerification,
|
||||
startMatrixVerification,
|
||||
} = await import("./verification.js"));
|
||||
});
|
||||
|
||||
@@ -250,6 +252,63 @@ describe("matrix verification actions", () => {
|
||||
expect(withStartedActionClientMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rehydrates DM verification requests before follow-up actions", async () => {
|
||||
const tracked = {
|
||||
completed: false,
|
||||
hasSas: false,
|
||||
id: "verification-1",
|
||||
phaseName: "requested",
|
||||
transactionId: "txn-dm",
|
||||
};
|
||||
const started = {
|
||||
...tracked,
|
||||
chosenMethod: "m.sas.v1",
|
||||
phaseName: "started",
|
||||
};
|
||||
const crypto = {
|
||||
ensureVerificationDmTracked: vi.fn(async () => tracked),
|
||||
startVerification: vi.fn(async () => started),
|
||||
};
|
||||
withStartedActionClientMock.mockImplementation(async (_opts, run) => {
|
||||
return await run({ crypto });
|
||||
});
|
||||
|
||||
await expect(
|
||||
startMatrixVerification("txn-dm", {
|
||||
verificationDmRoomId: "!dm:example.org",
|
||||
verificationDmUserId: "@alice:example.org",
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
id: "verification-1",
|
||||
phaseName: "started",
|
||||
});
|
||||
|
||||
expect(crypto.ensureVerificationDmTracked).toHaveBeenCalledWith({
|
||||
roomId: "!dm:example.org",
|
||||
userId: "@alice:example.org",
|
||||
});
|
||||
expect(crypto.startVerification).toHaveBeenCalledWith("txn-dm", "sas");
|
||||
});
|
||||
|
||||
it("requires complete DM lookup details for verification follow-up actions", async () => {
|
||||
const crypto = {
|
||||
ensureVerificationDmTracked: vi.fn(),
|
||||
startVerification: vi.fn(),
|
||||
};
|
||||
withStartedActionClientMock.mockImplementation(async (_opts, run) => {
|
||||
return await run({ crypto });
|
||||
});
|
||||
|
||||
await expect(
|
||||
startMatrixVerification("txn-dm", {
|
||||
verificationDmRoomId: "!dm:example.org",
|
||||
}),
|
||||
).rejects.toThrow("--user-id and --room-id must be provided together");
|
||||
|
||||
expect(crypto.ensureVerificationDmTracked).not.toHaveBeenCalled();
|
||||
expect(crypto.startVerification).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps self-verification in one started Matrix client session", async () => {
|
||||
const requested = {
|
||||
completed: false,
|
||||
@@ -420,6 +479,47 @@ describe("matrix verification actions", () => {
|
||||
expect(crypto.startVerification).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("fails immediately when an already-started self-verification uses a non-SAS method", async () => {
|
||||
const requested = {
|
||||
completed: false,
|
||||
hasSas: false,
|
||||
id: "verification-1",
|
||||
phaseName: "requested",
|
||||
transactionId: "tx-self",
|
||||
};
|
||||
const started = {
|
||||
...requested,
|
||||
chosenMethod: "m.reciprocate.v1",
|
||||
phaseName: "started",
|
||||
};
|
||||
const cancelled = {
|
||||
...started,
|
||||
phaseName: "cancelled",
|
||||
};
|
||||
const crypto = {
|
||||
cancelVerification: vi.fn(async () => cancelled),
|
||||
listVerifications: vi.fn(async () => [started]),
|
||||
requestVerification: vi.fn(async () => requested),
|
||||
startVerification: vi.fn(),
|
||||
};
|
||||
withStartedActionClientMock.mockImplementation(async (_opts, run) => {
|
||||
return await run({ crypto });
|
||||
});
|
||||
|
||||
await expect(
|
||||
runMatrixSelfVerification({ confirmSas: vi.fn(async () => true), timeoutMs: 500 }),
|
||||
).rejects.toThrow(
|
||||
"Matrix self-verification started without SAS while waiting to show SAS emoji or decimals (method: m.reciprocate.v1)",
|
||||
);
|
||||
|
||||
expect(crypto.listVerifications).toHaveBeenCalledTimes(1);
|
||||
expect(crypto.startVerification).not.toHaveBeenCalled();
|
||||
expect(crypto.cancelVerification).toHaveBeenCalledWith("verification-1", {
|
||||
code: "m.user",
|
||||
reason: "OpenClaw self-verification did not complete",
|
||||
});
|
||||
});
|
||||
|
||||
it("finalizes completed non-SAS self-verification without waiting for SAS", async () => {
|
||||
const completed = {
|
||||
completed: true,
|
||||
|
||||
@@ -12,6 +12,10 @@ const DEFAULT_MATRIX_SELF_VERIFICATION_TIMEOUT_MS = 180_000;
|
||||
|
||||
type MatrixCryptoActionFacade = NonNullable<import("../sdk.js").MatrixClient["crypto"]>;
|
||||
type MatrixActionClient = import("../sdk.js").MatrixClient;
|
||||
type MatrixVerificationDmLookupOpts = {
|
||||
verificationDmRoomId?: string;
|
||||
verificationDmUserId?: string;
|
||||
};
|
||||
|
||||
export type MatrixSelfVerificationResult = MatrixVerificationSummary & {
|
||||
deviceOwnerVerified: boolean;
|
||||
@@ -42,6 +46,26 @@ function resolveVerificationId(input: string): string {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
async function ensureMatrixVerificationDmTracked(
|
||||
crypto: MatrixCryptoActionFacade,
|
||||
opts: MatrixVerificationDmLookupOpts,
|
||||
): Promise<void> {
|
||||
const roomId = normalizeOptionalString(opts.verificationDmRoomId);
|
||||
const userId = normalizeOptionalString(opts.verificationDmUserId);
|
||||
if (Boolean(roomId) !== Boolean(userId)) {
|
||||
throw new Error("--user-id and --room-id must be provided together for Matrix DM verification");
|
||||
}
|
||||
if (!roomId || !userId) {
|
||||
return;
|
||||
}
|
||||
const tracked = await crypto.ensureVerificationDmTracked({ roomId, userId });
|
||||
if (!tracked) {
|
||||
throw new Error(
|
||||
`Matrix DM verification request not found for room ${roomId} and user ${userId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function isSameMatrixVerification(
|
||||
left: MatrixVerificationSummary,
|
||||
right: MatrixVerificationSummary,
|
||||
@@ -69,12 +93,38 @@ function isMatrixVerificationCancelled(summary: MatrixVerificationSummary): bool
|
||||
return summary.phaseName === "cancelled";
|
||||
}
|
||||
|
||||
function isMatrixSasMethod(method: string | null | undefined): boolean {
|
||||
return method === "m.sas.v1" || method === "sas";
|
||||
}
|
||||
|
||||
function getMatrixVerificationSasWaitFailure(
|
||||
summary: MatrixVerificationSummary,
|
||||
label: string,
|
||||
): string | null {
|
||||
if (summary.hasSas || summary.phaseName === "cancelled") {
|
||||
return null;
|
||||
}
|
||||
const method = summary.chosenMethod ? ` (method: ${summary.chosenMethod})` : "";
|
||||
if (summary.completed) {
|
||||
return `Matrix self-verification completed without SAS while waiting to ${label}${method}`;
|
||||
}
|
||||
if (
|
||||
summary.phaseName === "started" &&
|
||||
summary.chosenMethod &&
|
||||
!isMatrixSasMethod(summary.chosenMethod)
|
||||
) {
|
||||
return `Matrix self-verification started without SAS while waiting to ${label}${method}`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function waitForMatrixVerificationSummary(params: {
|
||||
crypto: MatrixCryptoActionFacade;
|
||||
label: string;
|
||||
request: MatrixVerificationSummary;
|
||||
timeoutMs: number;
|
||||
predicate: (summary: MatrixVerificationSummary) => boolean;
|
||||
reject?: (summary: MatrixVerificationSummary) => string | null;
|
||||
}): Promise<MatrixVerificationSummary> {
|
||||
const startedAt = Date.now();
|
||||
let last: MatrixVerificationSummary | undefined;
|
||||
@@ -93,6 +143,10 @@ async function waitForMatrixVerificationSummary(params: {
|
||||
}`,
|
||||
);
|
||||
}
|
||||
const rejection = params.reject?.(found);
|
||||
if (rejection) {
|
||||
throw new Error(rejection);
|
||||
}
|
||||
}
|
||||
await sleep(Math.min(250, Math.max(25, params.timeoutMs - (Date.now() - startedAt))));
|
||||
}
|
||||
@@ -246,12 +300,21 @@ export async function runMatrixSelfVerification(
|
||||
: ready;
|
||||
let sasSummary = started;
|
||||
if (!sasSummary.hasSas) {
|
||||
const sasFailure = getMatrixVerificationSasWaitFailure(
|
||||
sasSummary,
|
||||
"show SAS emoji or decimals",
|
||||
);
|
||||
if (sasFailure) {
|
||||
throw new Error(sasFailure);
|
||||
}
|
||||
sasSummary = await waitForMatrixVerificationSummary({
|
||||
crypto,
|
||||
label: "show SAS emoji or decimals",
|
||||
request: started,
|
||||
timeoutMs,
|
||||
predicate: (summary) => summary.hasSas,
|
||||
reject: (summary) =>
|
||||
getMatrixVerificationSasWaitFailure(summary, "show SAS emoji or decimals"),
|
||||
});
|
||||
}
|
||||
if (!sasSummary.sas) {
|
||||
@@ -289,20 +352,23 @@ export async function runMatrixSelfVerification(
|
||||
|
||||
export async function acceptMatrixVerification(
|
||||
requestId: string,
|
||||
opts: MatrixActionClientOpts = {},
|
||||
opts: MatrixActionClientOpts & MatrixVerificationDmLookupOpts = {},
|
||||
) {
|
||||
return await withStartedActionClient(opts, async (client) => {
|
||||
const crypto = requireCrypto(client, opts);
|
||||
await ensureMatrixVerificationDmTracked(crypto, opts);
|
||||
return await crypto.acceptVerification(resolveVerificationId(requestId));
|
||||
});
|
||||
}
|
||||
|
||||
export async function cancelMatrixVerification(
|
||||
requestId: string,
|
||||
opts: MatrixActionClientOpts & { reason?: string; code?: string } = {},
|
||||
opts: MatrixActionClientOpts &
|
||||
MatrixVerificationDmLookupOpts & { reason?: string; code?: string } = {},
|
||||
) {
|
||||
return await withStartedActionClient(opts, async (client) => {
|
||||
const crypto = requireCrypto(client, opts);
|
||||
await ensureMatrixVerificationDmTracked(crypto, opts);
|
||||
return await crypto.cancelVerification(resolveVerificationId(requestId), {
|
||||
reason: normalizeOptionalString(opts.reason),
|
||||
code: normalizeOptionalString(opts.code),
|
||||
@@ -312,20 +378,22 @@ export async function cancelMatrixVerification(
|
||||
|
||||
export async function startMatrixVerification(
|
||||
requestId: string,
|
||||
opts: MatrixActionClientOpts & { method?: "sas" } = {},
|
||||
opts: MatrixActionClientOpts & MatrixVerificationDmLookupOpts & { method?: "sas" } = {},
|
||||
) {
|
||||
return await withStartedActionClient(opts, async (client) => {
|
||||
const crypto = requireCrypto(client, opts);
|
||||
await ensureMatrixVerificationDmTracked(crypto, opts);
|
||||
return await crypto.startVerification(resolveVerificationId(requestId), opts.method ?? "sas");
|
||||
});
|
||||
}
|
||||
|
||||
export async function generateMatrixVerificationQr(
|
||||
requestId: string,
|
||||
opts: MatrixActionClientOpts = {},
|
||||
opts: MatrixActionClientOpts & MatrixVerificationDmLookupOpts = {},
|
||||
) {
|
||||
return await withStartedActionClient(opts, async (client) => {
|
||||
const crypto = requireCrypto(client, opts);
|
||||
await ensureMatrixVerificationDmTracked(crypto, opts);
|
||||
return await crypto.generateVerificationQr(resolveVerificationId(requestId));
|
||||
});
|
||||
}
|
||||
@@ -333,10 +401,11 @@ export async function generateMatrixVerificationQr(
|
||||
export async function scanMatrixVerificationQr(
|
||||
requestId: string,
|
||||
qrDataBase64: string,
|
||||
opts: MatrixActionClientOpts = {},
|
||||
opts: MatrixActionClientOpts & MatrixVerificationDmLookupOpts = {},
|
||||
) {
|
||||
return await withStartedActionClient(opts, async (client) => {
|
||||
const crypto = requireCrypto(client, opts);
|
||||
await ensureMatrixVerificationDmTracked(crypto, opts);
|
||||
const payload = qrDataBase64.trim();
|
||||
if (!payload) {
|
||||
throw new Error("Matrix QR data is required");
|
||||
@@ -347,40 +416,44 @@ export async function scanMatrixVerificationQr(
|
||||
|
||||
export async function getMatrixVerificationSas(
|
||||
requestId: string,
|
||||
opts: MatrixActionClientOpts = {},
|
||||
opts: MatrixActionClientOpts & MatrixVerificationDmLookupOpts = {},
|
||||
) {
|
||||
return await withStartedActionClient(opts, async (client) => {
|
||||
const crypto = requireCrypto(client, opts);
|
||||
await ensureMatrixVerificationDmTracked(crypto, opts);
|
||||
return await crypto.getVerificationSas(resolveVerificationId(requestId));
|
||||
});
|
||||
}
|
||||
|
||||
export async function confirmMatrixVerificationSas(
|
||||
requestId: string,
|
||||
opts: MatrixActionClientOpts = {},
|
||||
opts: MatrixActionClientOpts & MatrixVerificationDmLookupOpts = {},
|
||||
) {
|
||||
return await withStartedActionClient(opts, async (client) => {
|
||||
const crypto = requireCrypto(client, opts);
|
||||
await ensureMatrixVerificationDmTracked(crypto, opts);
|
||||
return await crypto.confirmVerificationSas(resolveVerificationId(requestId));
|
||||
});
|
||||
}
|
||||
|
||||
export async function mismatchMatrixVerificationSas(
|
||||
requestId: string,
|
||||
opts: MatrixActionClientOpts = {},
|
||||
opts: MatrixActionClientOpts & MatrixVerificationDmLookupOpts = {},
|
||||
) {
|
||||
return await withStartedActionClient(opts, async (client) => {
|
||||
const crypto = requireCrypto(client, opts);
|
||||
await ensureMatrixVerificationDmTracked(crypto, opts);
|
||||
return await crypto.mismatchVerificationSas(resolveVerificationId(requestId));
|
||||
});
|
||||
}
|
||||
|
||||
export async function confirmMatrixVerificationReciprocateQr(
|
||||
requestId: string,
|
||||
opts: MatrixActionClientOpts = {},
|
||||
opts: MatrixActionClientOpts & MatrixVerificationDmLookupOpts = {},
|
||||
) {
|
||||
return await withStartedActionClient(opts, async (client) => {
|
||||
const crypto = requireCrypto(client, opts);
|
||||
await ensureMatrixVerificationDmTracked(crypto, opts);
|
||||
return await crypto.confirmVerificationReciprocateQr(resolveVerificationId(requestId));
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user