koala73/worldmonitor · warning · ConvexError
ALREADY_REVOKED
ALREADY_REVOKED
Error message
ALREADY_REVOKED
What it means
Thrown by `revokeProMcpToken` when the token row exists, is owned by the caller, but already has `revokedAt` set. This guards against double-revoke producing confusing audit trails. Plain-string ConvexError; `err.data === "ALREADY_REVOKED"`. Distinct from NOT_FOUND (row missing/foreign) — here the row is found and owned but already in the revoked state.
Source
Thrown at convex/mcpProTokens.ts:240
/**
* Revoke a Pro MCP token row owned by the current user.
*
* Tenancy gate: the caller must own the row. Non-owner attempts surface
* as `NOT_FOUND` (don't leak existence of other users' tokens). Mirrors
* `apiKeys.revokeApiKey`.
*/
export const revokeProMcpToken = mutation({
args: { tokenId: v.id("mcpProTokens") },
handler: async (ctx, args) => {
const userId = await requireUserId(ctx);
const row = await ctx.db.get(args.tokenId);
if (!row || row.userId !== userId) {
throw new ConvexError("NOT_FOUND");
}
if (row.revokedAt) {
throw new ConvexError("ALREADY_REVOKED");
}
await ctx.db.patch(args.tokenId, { revokedAt: Date.now() });
return { ok: true };
},
});
View on GitHub (pinned to ffec79ac33)
Solutions
- Disable the revoke button immediately on click (optimistic UI) and refresh the list after.
- On `err.data === "ALREADY_REVOKED"`, treat as idempotent success — the desired state (revoked) is achieved.
- Use a request-in-flight guard so a double-click only fires one mutation.
- After revoke, optimistically mark the row revoked in local state before the server confirms.
Example fix
// before
const revoke = (id) => convex.mutation(api.mcpProTokens.revokeProMcpToken, { tokenId: id });
// after — idempotent handling + optimistic UI
const revoke = async (id) => {
setTokens((t) => t.map((x) => x.id === id ? { ...x, revokedAt: Date.now() } : x));
try {
await convex.mutation(api.mcpProTokens.revokeProMcpToken, { tokenId: id });
} catch (err) {
if (err.data === "ALREADY_REVOKED" || err.data === "NOT_FOUND") return; // desired state
throw err;
}
}; Defensive patterns
Strategy: try-catch
Validate before calling
// Optimistically mark revoked before the call to prevent double-revoke
setTokens((t) => t.map((x) => x._id === id ? { ...x, revokedAt: Date.now() } : x)); Type guard
function isAlreadyRevoked(row: { revokedAt?: number | null }): boolean {
return !!row.revokedAt;
} Try / catch
try {
await convex.mutation(api.mcpProTokens.revokeProMcpToken, { tokenId });
} catch (err) {
if (err.data === "ALREADY_REVOKED" || err.data === "NOT_FOUND") return; // desired state
throw err;
} Prevention
- Disable the revoke button on click (optimistic UI).
- Treat ALREADY_REVOKED and NOT_FOUND as idempotent success.
- Guard against double-clicks with a request-in-flight flag.
- The cap-rotation logic can auto-revoke a token — refresh the list after issue.
When it happens
Trigger: Calling revoke on a token that was already revoked: a double-click on the revoke button; a retry after a network timeout where the first call actually succeeded; the silent oldest-rotation in `issueProMcpToken` revoked it, and the user then manually revokes the same row.
Common situations: User clicks revoke twice before the UI updates; a client-side retry library re-fires the mutation after the first succeeded; a token was auto-rotated by the cap logic and the stale UI still shows it as active.
Related errors
- ALREADY_REVOKED
- [assignAndExportWave] waveLabel "${waveLabel}" already has s
- [initRamp] ramp already configured (active=${existing.active
- [_markPickComplete] run ${args.runId} is ${run.status}, expe
- ACCOUNT_NOT_FOUND
AI-assisted analysis of koala73/worldmonitor@ffec79ac33 (2026-08-12).
Data as JSON: /api/errors/6b0d505ad4327ee9.
Report an issue: GitHub.