koala73/worldmonitor · error · ConvexError
NOT_FOUND
NOT_FOUND
Error message
NOT_FOUND
What it means
Thrown by revokeApiKey when the requested key row does not exist (ctx.db.get returned null) OR exists but belongs to a different userId. The two cases are intentionally collapsed into one error to avoid leaking which keyIds exist for other users (a tenancy/ownership guard). This is the standard not-found / forbidden-as-not-found pattern.
Source
Thrown at convex/apiKeys.ts:180
keyPrefix: k.keyPrefix,
createdAt: k.createdAt,
lastUsedAt: k.lastUsedAt,
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.View on GitHub (pinned to ffec79ac33)
Solutions
- Refresh the key list via listApiKeys and pass a current, owned keyId.
- Confirm the keyId is a valid Convex id for the userApiKeys table.
- Treat NOT_FOUND as success if the goal is already achieved (key already gone).
- Check for concurrent revocation from another session/device.
Example fix
// before
await revokeApiKey(ctx, { keyId: staleId }); // NOT_FOUND
// after
const keys = await listApiKeys(ctx, {});
const target = keys.find(k => k.id === requestedId);
if (!target || target.revokedAt) return { ok: true }; // already gone
await revokeApiKey(ctx, { keyId: target.id }); Defensive patterns
Strategy: validation
Validate before calling
const keys = await listApiKeys(ctx, {});
const owned = keys.find(k => k.id === keyId);
if (!owned) return { ok: true }; // nothing to revoke
await revokeApiKey(ctx, { keyId: owned.id }); Try / catch
try {
await revokeApiKey(ctx, { keyId });
} catch (e) {
if (e instanceof ConvexError && e.message === "NOT_FOUND") {
// treat as already revoked/gone
} else throw e;
} Prevention
- Refresh the key list before offering revoke actions.
- Treat NOT_FOUND as success in idempotent revoke flows.
- Guard against passing stale ids from cached UI state.
When it happens
Trigger: Calling revokeApiKey with a keyId that was deleted, never existed, was passed as a malformed id, or belongs to another user; calling with a stale keyId from an outdated listApiKeys result.
Common situations: The UI passed a keyId from a stale list after another session revoked/deleted it; a cross-user bug passed the wrong keyId; the id was truncated or malformed in transit; the key never existed due to a typo.
Related errors
AI-assisted analysis of koala73/worldmonitor@ffec79ac33 (2026-08-12).
Data as JSON: /api/errors/04c46bfe7ecd10a9.
Report an issue: GitHub.