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

  1. Refresh the key list via listApiKeys and pass a current, owned keyId.
  2. Confirm the keyId is a valid Convex id for the userApiKeys table.
  3. Treat NOT_FOUND as success if the goal is already achieved (key already gone).
  4. 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

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.