koala73/worldmonitor · warning · ConvexError
ALREADY_REVOKED
ALREADY_REVOKED
Error message
ALREADY_REVOKED
What it means
revokeEmbedKey throws "ALREADY_REVOKED" when the target embedKeys document exists, is owned by the caller, but already has a truthy revokedAt timestamp. Revocation is a one-way transition; re-revoking would be a no-op at best, so the mutation rejects the call explicitly instead of silently patching the same value again.
Solutions
- Check key.revokedAt === null via listEmbedKeys before calling revokeEmbedKey, and skip keys already revoked.
- Treat the ALREADY_REVOKED ConvexError as a success signal in retry/reconcile paths — the desired end state (revoked) is already true.
- Update local/optimistic state immediately after a successful revoke so the UI cannot re-issue the mutation.
- In batch scripts, collect keyIds first and filter out those already revoked in a prior run.
Example fix
// before
await client.mutation(api.embedKeys.revokeEmbedKey, { keyId }); // throws ALREADY_REVOKED on retry
// after
try {
await client.mutation(api.embedKeys.revokeEmbedKey, { keyId });
} catch (e) {
if (!String(e).includes('ALREADY_REVOKED')) throw e; // idempotent: already in desired state
} Defensive patterns
Strategy: try-catch
Validate before calling
const keys = await client.query(api.embedKeys.listEmbedKeys, {});
const key = keys.find(k => k.id === keyId);
if (key && key.revokedAt !== null) return { ok: true }; // already revoked, nothing to do Type guard
function isActiveKey(key: { revokedAt: number | null }): boolean {
return key.revokedAt === null;
} Try / catch
try {
await client.mutation(api.embedKeys.revokeEmbedKey, { keyId });
} catch (e) {
if (String(e).includes('ALREADY_REVOKED')) return { ok: true }; // desired state already reached
throw e;
} Prevention
- Treat revocation as idempotent in client code: ALREADY_REVOKED means success.
- Update UI state to 'revoked' immediately after the first successful revoke.
- Debounce/revoke buttons and disable them while the mutation is pending.
- In batch revocation scripts, pre-filter keys whose revokedAt is already set.
When it happens
Trigger: Calling revokeEmbedKey a second time on the same keyId (double-click on the revoke button, a retried mutation after a network timeout in which the first attempt actually committed, or two UI components racing to revoke the same key).
Common situations: Optimistic UI not marking the key as revoked before the confirm dialog closes; a retry wrapper treating a lost response as failure and re-sending; cron/cleanup scripts revoking a list of keys where some were already revoked in an earlier run.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- COMPANY_MONITORING_CLASSIFICATION_REPLAY_CONFLICT
- DUPLICATE_KEY
- HTTP
- INVALID_API_KEY_SCOPES
- INVALID_HASH
AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15).
Data as JSON: /api/errors/32bdd25c16671a26.
Report an issue: GitHub.
Appendix: source
Thrown at convex/embedKeys.ts:167
revokedAt: k.revokedAt,
supersededAt: k.supersededAt,
allowedOrigins: k.allowedOrigins,
}));
},
});
/** Revoke an embed key owned by the current user. */
export const revokeEmbedKey = mutation({
args: { keyId: v.id("embedKeys") },
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 embed key by its SHA-256 hash.
* Returns the key row (with userId) if found and not revoked, else null.
* Used by the embed edge handler to resolve the embedding account.
*/
export const validateKeyByHash = internalQuery({
args: { keyHash: v.string() },View on GitHub (pinned to 7d06c8633d)