koala73/worldmonitor · error · ConvexError
NOT_FOUND
NOT_FOUND
Error message
NOT_FOUND
What it means
revokeEmbedKey fetches the embedKeys document by keyId and throws "NOT_FOUND" when no row exists with that ID or when the row belongs to a different user (key.userId !== the caller's userId from requireUserId). Ownership mismatches are deliberately reported as NOT_FOUND rather than a permission error so callers cannot probe for other users' key IDs. No database change is made.
Solutions
- Refresh the key list via listEmbedKeys and revoke using an id from that response — never a cached or hand-copied ID.
- Confirm you are authenticated as the user who owns the key (the check compares against the Clerk-derived userId).
- Verify the ID comes from the embedKeys table, not apiKeys or another table — Convex IDs are table-scoped.
- If the key exists but belongs to another user, you cannot revoke it; have the owner revoke it, and stop treating NOT_FOUND as a probe oracle for other users' keys.
Example fix
// before
await client.mutation(api.embedKeys.revokeEmbedKey, { keyId: props.keyId }); // stale/foreign id
// after
const keys = await client.query(api.embedKeys.listEmbedKeys, {});
const key = keys.find(k => k.id === props.keyId && k.revokedAt === null);
if (!key) return; // nothing to revoke for this user
await client.mutation(api.embedKeys.revokeEmbedKey, { keyId: key.id }); Defensive patterns
Strategy: validation
Validate before calling
const keys = await client.query(api.embedKeys.listEmbedKeys, {});
const ownsKey = keys.some(k => k.id === keyId);
if (!ownsKey) throw new Error('Refusing to revoke: key not in current user\'s embed key list'); Type guard
function isOwnedEmbedKeyId(keyId: string, keys: { id: string; revokedAt: number | null }[]): keyId is string {
return keys.some(k => k.id === keyId);
} Try / catch
try {
await client.mutation(api.embedKeys.revokeEmbedKey, { keyId });
} catch (e) {
if (String(e).includes('NOT_FOUND')) {
await refreshKeyList(); // stale id or foreign key — resync and inform the user
} else throw e;
} Prevention
- Only pass IDs obtained from listEmbedKeys for the current session/user.
- Refresh the key list after account switches and before destructive actions.
- Keep table IDs namespaced in client code (embed vs api keys) to avoid cross-table mistakes.
- Never hard-code key IDs from one environment into another.
When it happens
Trigger: Calling the revokeEmbedKey mutation with: a keyId from another user's account (or another environment); an ID that was already deleted or never existed; a key ID from the wrong table (e.g. an apiKey id passed to embedKeys.revokeEmbedKey); or a stale ID cached in the client after data was reset/migrated.
Common situations: Frontend holding a stale key list after switching Clerk accounts; tests reusing fixtures across users; passing an apiKeys table ID into the embed-keys revoker; or a hard-coded ID from a dev environment used against production.
Understand the failure class
Background: Record Not Found Errors: "not found", RecordNotFound, and "was not found" — what they mean and how to fix them — this error's family across 28 libraries.
Related errors
- INVALID_API_KEY_SCOPES
- This client was already revoked or no longer exists.
- NOT_FOUND
- CLAIM_NOT_FOUND
- EMBED_ACCESS_REQUIRED
AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15).
Data as JSON: /api/errors/9f5f88638c7d625f.
Report an issue: GitHub.
Appendix: source
Thrown at convex/embedKeys.ts:164
keyPrefix: k.keyPrefix,
createdAt: k.createdAt,
lastUsedAt: k.lastUsedAt,
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.View on GitHub (pinned to 7d06c8633d)