mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 00:47:57 +00:00
fix: surface CLI device code errors (#3166)
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { ConvexError } from "convex/values";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("./lib/access", () => ({
|
||||
@@ -208,6 +209,35 @@ describe("cliDeviceAuth approval", () => {
|
||||
expect(patch).not.toHaveBeenCalledWith("cliDeviceCodes:consumed", expect.anything());
|
||||
});
|
||||
|
||||
it.each([
|
||||
["expired", "Device code expired"],
|
||||
["consumed", "Device code already used"],
|
||||
["approved", "Device code already authorized"],
|
||||
["denied", "Device code was denied"],
|
||||
])("surfaces %s codes as user-facing errors", async (status, message) => {
|
||||
const now = Date.now();
|
||||
const { ctx } = makeCtx([
|
||||
{
|
||||
_id: `cliDeviceCodes:${status}`,
|
||||
_creationTime: now - 1_000,
|
||||
status,
|
||||
userCode: "Q639-NBSX",
|
||||
createdAt: now - 1_000,
|
||||
expiresAt: now + 60_000,
|
||||
},
|
||||
]);
|
||||
|
||||
let caught: unknown;
|
||||
try {
|
||||
await approveHandler(ctx, { userCode: "Q639-NBSX" });
|
||||
} catch (error) {
|
||||
caught = error;
|
||||
}
|
||||
|
||||
expect(caught).toBeInstanceOf(ConvexError);
|
||||
expect((caught as ConvexError<string>).data).toBe(message);
|
||||
});
|
||||
|
||||
it("denies the newest active pending row when duplicate user codes exist", async () => {
|
||||
const now = Date.now();
|
||||
const { ctx, patch } = makeCtx([
|
||||
|
||||
+11
-11
@@ -1,4 +1,4 @@
|
||||
import { v } from "convex/values";
|
||||
import { ConvexError, v } from "convex/values";
|
||||
import type { Doc } from "./_generated/dataModel";
|
||||
import type { MutationCtx } from "./_generated/server";
|
||||
import { internalMutation, mutation } from "./functions";
|
||||
@@ -88,19 +88,19 @@ export const approve = mutation({
|
||||
handler: async (ctx, args) => {
|
||||
const { userId } = await requireUser(ctx);
|
||||
const normalized = normalizeUserCode(args.userCode);
|
||||
if (!normalized) throw new Error("Code required");
|
||||
if (!normalized) throw new ConvexError("Code required");
|
||||
|
||||
const userCodeHash = await hashToken(normalized);
|
||||
const now = Date.now();
|
||||
const rows = await expireStaleRows(ctx, await getRowsByUserCodeHash(ctx, userCodeHash), now);
|
||||
const row =
|
||||
pickLatestRow(rows, now, "pending") ?? pickLatestRow(rows, now) ?? pickLatestRow(rows);
|
||||
if (!row) throw new Error("Device code not found");
|
||||
if (row.expiresAt <= now) throw new Error("Device code expired");
|
||||
if (row.status === "expired") throw new Error("Device code expired");
|
||||
if (row.status === "consumed") throw new Error("Device code already used");
|
||||
if (row.status === "approved") throw new Error("Device code already authorized");
|
||||
if (row.status === "denied") throw new Error("Device code was denied");
|
||||
if (!row) throw new ConvexError("Device code not found");
|
||||
if (row.expiresAt <= now) throw new ConvexError("Device code expired");
|
||||
if (row.status === "expired") throw new ConvexError("Device code expired");
|
||||
if (row.status === "consumed") throw new ConvexError("Device code already used");
|
||||
if (row.status === "approved") throw new ConvexError("Device code already authorized");
|
||||
if (row.status === "denied") throw new ConvexError("Device code was denied");
|
||||
|
||||
await ctx.db.patch(row._id, {
|
||||
status: "approved",
|
||||
@@ -116,14 +116,14 @@ export const deny = mutation({
|
||||
handler: async (ctx, args) => {
|
||||
await requireUser(ctx);
|
||||
const normalized = normalizeUserCode(args.userCode);
|
||||
if (!normalized) throw new Error("Code required");
|
||||
if (!normalized) throw new ConvexError("Code required");
|
||||
const userCodeHash = await hashToken(normalized);
|
||||
const now = Date.now();
|
||||
const rows = await expireStaleRows(ctx, await getRowsByUserCodeHash(ctx, userCodeHash), now);
|
||||
const row =
|
||||
pickLatestRow(rows, now, "pending") ?? pickLatestRow(rows, now) ?? pickLatestRow(rows);
|
||||
if (!row) throw new Error("Device code not found");
|
||||
if (row.status === "approved") throw new Error("Device code already authorized");
|
||||
if (!row) throw new ConvexError("Device code not found");
|
||||
if (row.status === "approved") throw new ConvexError("Device code already authorized");
|
||||
if (row.status === "pending") {
|
||||
await ctx.db.patch(row._id, { status: "denied", deniedAt: now });
|
||||
}
|
||||
|
||||
@@ -148,4 +148,23 @@ describe("CliDeviceAuth", () => {
|
||||
expect(authorize).toHaveProperty("disabled", true);
|
||||
expect(deny).toHaveProperty("disabled", true);
|
||||
});
|
||||
|
||||
it("shows a clean message when a device code was already used", async () => {
|
||||
approveMock.mockRejectedValue(
|
||||
new Error(
|
||||
"[CONVEX M(cliDeviceAuth:approve)] [Request ID: test] Server Error Called by client ConvexError: Device code already used",
|
||||
),
|
||||
);
|
||||
denyMock.mockResolvedValue(undefined);
|
||||
useMutationMock.mockImplementation((mutation: string) =>
|
||||
mutation === "approve" ? approveMock : denyMock,
|
||||
);
|
||||
|
||||
render(<CliDeviceAuth />);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Authorize" }));
|
||||
|
||||
expect(await screen.findByText("Device code already used")).toBeTruthy();
|
||||
expect(screen.queryByText(/Server Error Called by client/)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "../../components/ui/ca
|
||||
import { Input } from "../../components/ui/input";
|
||||
import { Label } from "../../components/ui/label";
|
||||
import { isCliDeviceUserCode } from "../../lib/cliDeviceCode";
|
||||
import { getUserFacingConvexError } from "../../lib/convexError";
|
||||
import { useAuthStatus } from "../../lib/useAuthStatus";
|
||||
|
||||
export const Route = createFileRoute("/cli/device")({
|
||||
@@ -45,7 +46,7 @@ export function CliDeviceAuth() {
|
||||
setIsComplete(true);
|
||||
setStatus("Authorized. You can return to your terminal.");
|
||||
} catch (error) {
|
||||
setStatus(error instanceof Error ? error.message : "Authorization failed.");
|
||||
setStatus(getUserFacingConvexError(error, "Authorization failed."));
|
||||
} finally {
|
||||
actionInFlight.current = false;
|
||||
setPendingAction(null);
|
||||
@@ -64,7 +65,7 @@ export function CliDeviceAuth() {
|
||||
setIsComplete(true);
|
||||
setStatus("Denied. You can close this page.");
|
||||
} catch (error) {
|
||||
setStatus(error instanceof Error ? error.message : "Deny failed.");
|
||||
setStatus(getUserFacingConvexError(error, "Deny failed."));
|
||||
} finally {
|
||||
actionInFlight.current = false;
|
||||
setPendingAction(null);
|
||||
|
||||
Reference in New Issue
Block a user