mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 12:40:43 +00:00
fix: handle Matrix verification edge cases
This commit is contained in:
@@ -420,6 +420,45 @@ describe("matrix verification actions", () => {
|
||||
expect(crypto.startVerification).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("finalizes completed non-SAS self-verification without waiting for SAS", async () => {
|
||||
const completed = {
|
||||
completed: true,
|
||||
hasSas: false,
|
||||
id: "verification-1",
|
||||
phaseName: "done",
|
||||
transactionId: "tx-self",
|
||||
};
|
||||
const crypto = {
|
||||
confirmVerificationSas: vi.fn(),
|
||||
listVerifications: vi.fn(async () => []),
|
||||
requestVerification: vi.fn(async () => completed),
|
||||
startVerification: vi.fn(),
|
||||
};
|
||||
const confirmSas = vi.fn(async () => true);
|
||||
const bootstrapOwnDeviceVerification = vi.fn(async () => ({
|
||||
success: true,
|
||||
verification: mockVerifiedOwnerStatus(),
|
||||
}));
|
||||
withStartedActionClientMock.mockImplementation(async (_opts, run) => {
|
||||
return await run({
|
||||
bootstrapOwnDeviceVerification,
|
||||
crypto,
|
||||
getOwnDeviceVerificationStatus: vi.fn(async () => mockVerifiedOwnerStatus()),
|
||||
});
|
||||
});
|
||||
|
||||
await expect(runMatrixSelfVerification({ confirmSas, timeoutMs: 500 })).resolves.toMatchObject({
|
||||
completed: true,
|
||||
deviceOwnerVerified: true,
|
||||
id: "verification-1",
|
||||
});
|
||||
|
||||
expect(crypto.listVerifications).not.toHaveBeenCalled();
|
||||
expect(crypto.startVerification).not.toHaveBeenCalled();
|
||||
expect(crypto.confirmVerificationSas).not.toHaveBeenCalled();
|
||||
expect(confirmSas).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("allows completed self-verification when only backup health remains degraded", async () => {
|
||||
const requested = {
|
||||
completed: false,
|
||||
@@ -544,4 +583,71 @@ describe("matrix verification actions", () => {
|
||||
reason: "OpenClaw self-verification did not complete",
|
||||
});
|
||||
});
|
||||
|
||||
it("fails immediately when the self-verification request is cancelled while waiting", async () => {
|
||||
const requested = {
|
||||
completed: false,
|
||||
hasSas: false,
|
||||
id: "verification-1",
|
||||
phaseName: "requested",
|
||||
transactionId: "tx-self",
|
||||
};
|
||||
const cancelled = {
|
||||
...requested,
|
||||
error: "Remote cancelled",
|
||||
pending: false,
|
||||
phaseName: "cancelled",
|
||||
};
|
||||
const crypto = {
|
||||
cancelVerification: vi.fn(async () => cancelled),
|
||||
listVerifications: vi.fn(async () => [cancelled]),
|
||||
requestVerification: vi.fn(async () => requested),
|
||||
};
|
||||
withStartedActionClientMock.mockImplementation(async (_opts, run) => {
|
||||
return await run({ crypto });
|
||||
});
|
||||
|
||||
await expect(
|
||||
runMatrixSelfVerification({ confirmSas: vi.fn(async () => true), timeoutMs: 500 }),
|
||||
).rejects.toThrow("Matrix self-verification was cancelled: Remote cancelled");
|
||||
|
||||
expect(crypto.listVerifications).toHaveBeenCalledTimes(1);
|
||||
expect(crypto.cancelVerification).toHaveBeenCalledWith("verification-1", {
|
||||
code: "m.user",
|
||||
reason: "OpenClaw self-verification did not complete",
|
||||
});
|
||||
});
|
||||
|
||||
it("cancels the request when SAS mismatch submission fails", async () => {
|
||||
const sas = {
|
||||
completed: false,
|
||||
hasSas: true,
|
||||
id: "verification-1",
|
||||
phaseName: "started",
|
||||
sas: {
|
||||
decimal: [1, 2, 3],
|
||||
},
|
||||
transactionId: "tx-self",
|
||||
};
|
||||
const crypto = {
|
||||
cancelVerification: vi.fn(async () => sas),
|
||||
listVerifications: vi.fn(async () => [sas]),
|
||||
mismatchVerificationSas: vi.fn(async () => {
|
||||
throw new Error("failed to send SAS mismatch");
|
||||
}),
|
||||
requestVerification: vi.fn(async () => sas),
|
||||
};
|
||||
withStartedActionClientMock.mockImplementation(async (_opts, run) => {
|
||||
return await run({ crypto });
|
||||
});
|
||||
|
||||
await expect(
|
||||
runMatrixSelfVerification({ confirmSas: vi.fn(async () => false), timeoutMs: 500 }),
|
||||
).rejects.toThrow("failed to send SAS mismatch");
|
||||
|
||||
expect(crypto.cancelVerification).toHaveBeenCalledWith("verification-1", {
|
||||
code: "m.user",
|
||||
reason: "OpenClaw self-verification did not complete",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -65,6 +65,10 @@ function shouldStartMatrixSasVerification(summary: MatrixVerificationSummary): b
|
||||
return !summary.hasSas && summary.phaseName !== "started" && !summary.completed;
|
||||
}
|
||||
|
||||
function isMatrixVerificationCancelled(summary: MatrixVerificationSummary): boolean {
|
||||
return summary.phaseName === "cancelled";
|
||||
}
|
||||
|
||||
async function waitForMatrixVerificationSummary(params: {
|
||||
crypto: MatrixCryptoActionFacade;
|
||||
label: string;
|
||||
@@ -82,6 +86,13 @@ async function waitForMatrixVerificationSummary(params: {
|
||||
if (params.predicate(found)) {
|
||||
return found;
|
||||
}
|
||||
if (isMatrixVerificationCancelled(found)) {
|
||||
throw new Error(
|
||||
`Matrix self-verification was cancelled${
|
||||
found.error ? `: ${found.error}` : ` while waiting to ${params.label}`
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
await sleep(Math.min(250, Math.max(25, params.timeoutMs - (Date.now() - startedAt))));
|
||||
}
|
||||
@@ -138,6 +149,33 @@ async function cancelMatrixSelfVerificationOnFailure(params: {
|
||||
.catch(() => undefined);
|
||||
}
|
||||
|
||||
async function completeMatrixSelfVerification(params: {
|
||||
client: MatrixActionClient;
|
||||
completed: MatrixVerificationSummary;
|
||||
timeoutMs: number;
|
||||
}): Promise<MatrixSelfVerificationResult> {
|
||||
const bootstrap = await params.client.bootstrapOwnDeviceVerification({
|
||||
allowAutomaticCrossSigningReset: false,
|
||||
verifyOwnIdentity: true,
|
||||
});
|
||||
if (!bootstrap.verification.verified) {
|
||||
throw new Error(
|
||||
`Matrix self-verification completed, but full Matrix identity trust is still incomplete: ${
|
||||
bootstrap.error ?? formatMatrixOwnerVerificationDiagnostics(bootstrap.verification)
|
||||
}`,
|
||||
);
|
||||
}
|
||||
const ownerVerification = await waitForMatrixOwnerVerificationStatus({
|
||||
client: params.client,
|
||||
timeoutMs: params.timeoutMs,
|
||||
});
|
||||
return {
|
||||
...params.completed,
|
||||
deviceOwnerVerified: ownerVerification.verified,
|
||||
ownerVerification,
|
||||
};
|
||||
}
|
||||
|
||||
export async function listMatrixVerifications(opts: MatrixActionClientOpts = {}) {
|
||||
return await withStartedActionClient(opts, async (client) => {
|
||||
const crypto = requireCrypto(client, opts);
|
||||
@@ -187,18 +225,22 @@ export async function runMatrixSelfVerification(
|
||||
requested = await crypto.requestVerification({ ownUser: true });
|
||||
await params.onRequested?.(requested);
|
||||
|
||||
let ready = requested;
|
||||
if (!ready.hasSas) {
|
||||
ready = await waitForMatrixVerificationSummary({
|
||||
crypto,
|
||||
label: "be accepted in another Matrix client",
|
||||
request: requested,
|
||||
timeoutMs,
|
||||
predicate: isMatrixVerificationReadyForSas,
|
||||
});
|
||||
}
|
||||
const ready = isMatrixVerificationReadyForSas(requested)
|
||||
? requested
|
||||
: await waitForMatrixVerificationSummary({
|
||||
crypto,
|
||||
label: "be accepted in another Matrix client",
|
||||
request: requested,
|
||||
timeoutMs,
|
||||
predicate: isMatrixVerificationReadyForSas,
|
||||
});
|
||||
await params.onReady?.(ready);
|
||||
|
||||
if (ready.completed) {
|
||||
requestCompleted = true;
|
||||
return await completeMatrixSelfVerification({ client, completed: ready, timeoutMs });
|
||||
}
|
||||
|
||||
const started = shouldStartMatrixSasVerification(ready)
|
||||
? await crypto.startVerification(ready.id, "sas")
|
||||
: ready;
|
||||
@@ -219,8 +261,8 @@ export async function runMatrixSelfVerification(
|
||||
|
||||
const matched = await params.confirmSas(sasSummary.sas, sasSummary);
|
||||
if (!matched) {
|
||||
handledByMismatch = true;
|
||||
await crypto.mismatchVerificationSas(sasSummary.id);
|
||||
handledByMismatch = true;
|
||||
throw new Error("Matrix SAS verification was not confirmed.");
|
||||
}
|
||||
|
||||
@@ -235,23 +277,7 @@ export async function runMatrixSelfVerification(
|
||||
predicate: (summary) => summary.completed,
|
||||
});
|
||||
requestCompleted = true;
|
||||
const bootstrap = await client.bootstrapOwnDeviceVerification({
|
||||
allowAutomaticCrossSigningReset: false,
|
||||
verifyOwnIdentity: true,
|
||||
});
|
||||
if (!bootstrap.verification.verified) {
|
||||
throw new Error(
|
||||
`Matrix self-verification completed, but full Matrix identity trust is still incomplete: ${
|
||||
bootstrap.error ?? formatMatrixOwnerVerificationDiagnostics(bootstrap.verification)
|
||||
}`,
|
||||
);
|
||||
}
|
||||
const ownerVerification = await waitForMatrixOwnerVerificationStatus({ client, timeoutMs });
|
||||
return {
|
||||
...completed,
|
||||
deviceOwnerVerified: ownerVerification.verified,
|
||||
ownerVerification,
|
||||
};
|
||||
return await completeMatrixSelfVerification({ client, completed, timeoutMs });
|
||||
} catch (error) {
|
||||
if (!requestCompleted && !handledByMismatch) {
|
||||
await cancelMatrixSelfVerificationOnFailure({ crypto, request: requested });
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { spawn as startOpenClawCliProcess } from "node:child_process";
|
||||
import path from "node:path";
|
||||
import { setTimeout as sleep } from "node:timers/promises";
|
||||
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
@@ -69,7 +69,7 @@ export function startMatrixQaOpenClawCli(params: {
|
||||
}
|
||||
| undefined;
|
||||
|
||||
const child = spawn(process.execPath, [distEntryPath, ...params.args], {
|
||||
const child = startOpenClawCliProcess(process.execPath, [distEntryPath, ...params.args], {
|
||||
cwd,
|
||||
env: params.env,
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
|
||||
Reference in New Issue
Block a user