Stirling-Tools/Stirling-PDF · error · Error

Member must belong to a team to become its owner

Error message

Member must belong to a team to become its owner

What it means

Thrown by changeMemberRole() (users.ts) when the target role is 'team_owner' but member.teamId is falsy. Team ownership is assigned via POST /api/v1/team/setOwner which requires a teamId, so promoting a teamless member to owner is impossible. The guard runs before any backend call, leaving the user unchanged.

Source

Thrown at frontend/editor/src/portal/api/users.ts:370

    // Demote to web-only; drop any leadership first so no team is left ownerless-by-a-guest.
    if (holdsLeader && member.teamId) {
      await apiClient.local.form("/api/v1/team/removeOwner", {
        teamId: String(member.teamId),
        userId: member.id,
      });
    }
    await setAuthority(member.username, "ROLE_WEB_ONLY_USER");
    return;
  }

  if (target === "admin") {
    if (!holdsAdmin) await setAuthority(member.username, "ROLE_ADMIN");
    return; // any LEADER membership is harmless; an admin owns everything anyway
  }

  if (target === "team_owner") {
    if (!member.teamId) {
      throw new Error("Member must belong to a team to become its owner");
    }
    // Assign ownership first so a 400 (e.g. system team) leaves the user unchanged.
    await apiClient.local.form("/api/v1/team/setOwner", {
      teamId: String(member.teamId),
      userId: member.id,
    });
    if (holdsAdmin || holdsWebOnly)
      await setAuthority(member.username, "ROLE_USER");
    return;
  }

  // target === "member": drop any leadership, and normalise a non-ROLE_USER authority.
  if (holdsLeader && member.teamId) {
    await apiClient.local.form("/api/v1/team/removeOwner", {
      teamId: String(member.teamId),
      userId: member.id,
    });
  }

View on GitHub (pinned to 9ef20dcab8)

Solutions

  1. Assign the member to a team first (moveMemberToTeam), then retry the role change to team_owner.
  2. Disable the 'team_owner' role option in the UI for members without a teamId.
  3. Validate member.teamId before calling changeMemberRole and surface a clear inline message.

Example fix

// before — throws for teamless members
if (target === "team_owner") {
  if (!member.teamId) throw new Error("Member must belong to a team to become its owner");
  ...
}

// after — guard at the call site, block the action in the UI
async function promoteToOwner(member: Member) {
  if (!member.teamId) throw new TeamRequiredError(member.id);
  return changeMemberRole(member, "team_owner");
}
Defensive patterns

Strategy: validation

Validate before calling

import type { Member } from "@portal/api/users";
function canBeTeamOwner(member: Member): boolean { return member.teamId != null; }
// before changeMemberRole: if (target === 'team_owner' && !canBeTeamOwner(member)) blockUI();

Type guard

function isTeamRequired(e: unknown): boolean {
  return e instanceof Error && /must belong to a team/.test(e.message);
}

Try / catch

try { await changeMemberRole(member, 'team_owner'); }
catch (e) { if (isTeamRequired(e)) { notify('Assign the member to a team first.'); return; } throw e; }

Prevention

When it happens

Trigger: An admin attempts to set a member's role to 'team_owner' for a member whose teamId is undefined/null (not assigned to any team).

Common situations: A member was created/invited without a team; the member's team was dissolved leaving teamId stale/null; the UI allowed selecting 'team_owner' for a teamless member.

Related errors


AI-assisted analysis of Stirling-Tools/Stirling-PDF@9ef20dcab8 (2026-08-13). Data as JSON: /api/errors/29ee4f72bb849f1f. Report an issue: GitHub.