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
- Assign the member to a team first (moveMemberToTeam), then retry the role change to team_owner.
- Disable the 'team_owner' role option in the UI for members without a teamId.
- 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
- Disable the 'team_owner' role option in the UI for members without a teamId.
- Assign the member to a team (moveMemberToTeam) before promoting.
- Validate member.teamId at the call site to give a clear inline message.
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
- Member has no backend identity
- No team to invite to
- Member has no team to be removed from
- No team resolved yet
- No automation configuration provided
AI-assisted analysis of Stirling-Tools/Stirling-PDF@9ef20dcab8 (2026-08-13).
Data as JSON: /api/errors/29ee4f72bb849f1f.
Report an issue: GitHub.