koala73/worldmonitor · warning · ConvexError
ALREADY_REVOKED
ALREADY_REVOKED
Error message
ALREADY_REVOKED
What it means
Thrown by revokeApiKey when the key row exists and is owned by the user, but its revokedAt field is already set (non-null). This is an idempotency guard preventing double-revocation; the key is already in the desired terminal state.
Source
Thrown at convex/apiKeys.ts:183
revokedAt: k.revokedAt,
scopes: k.scopes,
companyMonitoringAccountId: k.companyMonitoringAccountId,
}));
},
});
/** Revoke a key owned by the current user. */
export const revokeApiKey = mutation({
args: { keyId: v.id("userApiKeys") },
handler: async (ctx, args) => {
const userId = await requireUserId(ctx);
const key = await ctx.db.get(args.keyId);
if (!key || key.userId !== userId) {
throw new ConvexError("NOT_FOUND");
}
if (key.revokedAt) {
throw new ConvexError("ALREADY_REVOKED");
}
await ctx.db.patch(args.keyId, { revokedAt: Date.now() });
return { ok: true, keyHash: key.keyHash };
},
});
// ---------------------------------------------------------------------------
// Internal (service-to-service) — called from HTTP actions / middleware
// ---------------------------------------------------------------------------
/**
* Look up an API key by its SHA-256 hash.
* Returns the key row (with userId) if found and not revoked, else null.
* Used by the edge gateway to validate incoming API keys.
*/
export const validateKeyByHash = internalQuery({
args: { keyHash: v.string() },View on GitHub (pinned to ffec79ac33)
Solutions
- Treat ALREADY_REVOKED as the desired terminal state — the key is revoked, no further action needed.
- Guard the UI against double-submission (disable the button after first click).
- Check key.revokedAt from listApiKeys before calling revokeApiKey.
Example fix
// before
await revokeApiKey(ctx, { keyId }); // ALREADY_REVOKED on second click
// after — idempotent wrapper
try {
await revokeApiKey(ctx, { keyId });
} catch (e) {
if (e.message !== "ALREADY_REVOKED") throw e;
// already revoked — treat as success
} Defensive patterns
Strategy: try-catch
Validate before calling
const keys = await listApiKeys(ctx, {});
const target = keys.find(k => k.id === keyId);
if (!target || target.revokedAt) return { ok: true };
await revokeApiKey(ctx, { keyId }); Try / catch
try {
await revokeApiKey(ctx, { keyId });
} catch (e) {
if (e instanceof ConvexError && e.message === "ALREADY_REVOKED") return { ok: true };
throw e;
} Prevention
- Disable the revoke button immediately after first click.
- Check revokedAt before calling revokeApiKey.
- Treat ALREADY_REVOKED as the desired terminal state.
When it happens
Trigger: Calling revokeApiKey twice on the same key; calling revoke after the key was auto-revoked by the KEY_LIMIT overflow-convergence path; a UI double-click firing two revoke requests.
Common situations: User clicked revoke twice; the create-key overflow handler revoked this key and the user then tries to manually revoke it; a retry of a previously-successful revoke.
Related errors
AI-assisted analysis of koala73/worldmonitor@ffec79ac33 (2026-08-12).
Data as JSON: /api/errors/29e9d1f0ed074b0f.
Report an issue: GitHub.