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

  1. Treat ALREADY_REVOKED as the desired terminal state — the key is revoked, no further action needed.
  2. Guard the UI against double-submission (disable the button after first click).
  3. 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

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.