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

  1. Refresh the key list via listEmbedKeys and revoke using an id from that response — never a cached or hand-copied ID.
  2. Confirm you are authenticated as the user who owns the key (the check compares against the Clerk-derived userId).
  3. Verify the ID comes from the embedKeys table, not apiKeys or another table — Convex IDs are table-scoped.
  4. 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

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


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)