different-ai/openwork · error

Ownership can only be transferred to an active super-admin.

Error message

Ownership can only be transferred to an active super-admin.

What it means

After the owner check, transferOwnership validates the target member: the member must exist in orgContext.members, have a joinedAt timestamp (active member), and resolve to a super-admin role via getOrgAccessFlags. If any of these fail, the provider throws "Ownership can only be transferred to an active super-admin." This enforces that ownership only passes to an active super-admin, never to a pending invite, non-member, or lower role.

Source

Thrown at ee/apps/den-web/app/(den)/dashboard/_providers/org-dashboard-provider.tsx:725

        `/v1/members/${encodeURIComponent(memberId)}`,
        { method: "DELETE" },
        12000,
      );

      if (response.status !== 204 && !response.ok) {
        throw getRequestError(payload, response, `Failed to remove member (${response.status}).`);
      }
    });
  }

  async function transferOwnership(memberId: string) {
    if (!getCurrentAccess().canTransferOwnership) {
      throw new Error("Only the workspace owner can transfer ownership.");
    }
    const target = ensureTargetIsNotOwner(memberId);
    const targetAccess = getOrgAccessFlags(target?.role ?? "member", target?.isOwner ?? false, orgContext?.roles);
    if (!target || !target.joinedAt || !targetAccess.isSuperAdmin) {
      throw new Error("Ownership can only be transferred to an active super-admin.");
    }

    await runMutation("transfer-ownership", async () => {
      ensureActiveOrganizationSelected();
      const { response, payload } = await requestJson(
        `/v1/members/${encodeURIComponent(memberId)}/transfer-ownership`,
        { method: "POST", body: JSON.stringify({}) },
        12000,
      );

      if (!response.ok) {
        throw getRequestError(payload, response, `Failed to transfer ownership (${response.status}).`);
      }
    });
  }

  async function createRole(input: { roleName: string; permission: Record<string, string[]> }) {
    if (!getCurrentAccess().canManageRoles) {

View on GitHub (pinned to 2b7df46e8a)

Solutions

  1. Promote the target to super-admin first, then transfer ownership.
  2. Only transfer to a member who has accepted their invite (joinedAt set).
  3. Re-check the member list and use a fresh memberId if the target recently joined or left.
  4. Filter the transfer UI to active super-admins only.

Example fix

// before
handleTransferOwnership(inviteeId); // pending invite

// after
const target = members.find((m) => m.id === inviteeId);
const ok = target?.joinedAt && getOrgAccessFlags(target.role, target.isOwner, roles).isSuperAdmin;
if (!ok) {
  showError("Ownership can only be transferred to an active super-admin.");
  return;
}
handleTransferOwnership(inviteeId);
Defensive patterns

Strategy: validation

Validate before calling

const target = members.find((m) => m.id === memberId);
const eligible = !!target?.joinedAt && getOrgAccessFlags(target.role, target.isOwner, roles).isSuperAdmin;
if (!eligible) { /* block call: promote to super-admin first */ }

Type guard

function isEligibleOwnerTarget(m: Member | undefined, roles: OrgRole[]): m is Member & { joinedAt: string } {
  return !!m && !!m.joinedAt && !m.isOwner && getOrgAccessFlags(m.role, m.isOwner, roles).isSuperAdmin;
}

Try / catch

try {
  await transferOwnership(memberId);
} catch (e) {
  if (e instanceof Error && e.message.includes("active super-admin")) {
    showNotice("Promote the member to super-admin before transferring ownership.");
  } else throw e;
}

Prevention

When it happens

Trigger: Calling transferOwnership with a memberId that is not in the member list, belongs to a member who has not joined (no joinedAt), or whose role is not a super-admin (e.g. member/admin/custom role).

Common situations: Selecting a pending invitation as the transfer target; attempting to promote a regular member directly to owner in one step (must first grant super-admin); memberId from a stale member list after the target left the org; passing a wrong/typo'd id programmatically.

Related errors


AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01). Data as JSON: /api/errors/cb44516cb693dbf1. Report an issue: GitHub.