Interface ITeamDirectoryService
The caller's own teams — the interface a component, controller or MCP provider should inject to
list them. Scope-filtered rather than scope-gated: each team is included only if the caller's
membership in that team grants team:read.
public interface ITeamDirectoryService
Remarks
Separate from ITeamManagementService because that interface is wholly team-bound — every operation names a team in its first argument, which is what lets one registration authorize all of them. This one names no team, so it cannot be gated the same way and does not belong there.
It cannot use [RequireScope] for the same reason, and a principal carries scope claims only for
the *selected* team, so there is nothing in the claims to check the others against. The scopes are
recomputed per team from the caller's membership instead — the same inputs the claims builder uses.
Methods
GetTeamsAsync()
The same teams without their rosters, for a caller that does not know the host's member type.
IAsyncEnumerable<ITeam> GetTeamsAsync()
Returns
Remarks
Filtered identically — it runs the generic overload and drops the rosters, rather than reproducing the per-team scope recomputation. Two copies of that rule would be two chances for it to drift.
GetTeamsAsync<TMember>()
The caller's teams, omitting any where their membership does not grant team:read. A team is
omitted whole rather than returned without its roster: the scope covers "team details and members"
together, so a half-visible team would be a state nothing else in the model has.
IAsyncEnumerable<ITeam<TMember>> GetTeamsAsync<TMember>() where TMember : ITeamMember
Returns
- IAsyncEnumerable<ITeam<TMember>>
Type Parameters
TMember
IsSuspendedAsync(string)
Whether the caller's own membership in this team is suspended. A suspended member still sees the team here and in the selector — they simply hold no scopes in it.
Task<bool> IsSuspendedAsync(string teamKey)
Parameters
teamKeystring
Returns
Remarks
Ungated on purpose, and it has to be. A suspended member holds no team scopes at all, so any scope-checked read would refuse them — and then nothing could tell them why their team stopped working. This asks only about the caller's own membership, which is not somebody else's information to protect.
Suspension is invisible to GetTeamsAsync()'s team:read filter by design: that
filter recomputes scopes from access level, roles and overrides, none of which suspension touches.
The team therefore stays listed with no special case, which is exactly the intent.
LeaveTeamAsync(string)
Leaves the team. Refuses the Owner, who must transfer ownership first, and the last administrator of a team with no owner.
Task LeaveTeamAsync(string teamKey)
Parameters
teamKeystring
Returns
Remarks
The one mutation on this interface, and it belongs here rather than on
ITeamManagementService. That interface is gated: ScopeProxy throws on an
unattributed method, so every operation there must name a scope. Leaving can name none —
member:manage, which used to authorize it as a self-removal, is registered at
Administrator and so is held by nobody who most needs to leave, and a
suspended member holds no scope whatsoever.
So it sits beside IsSuspendedAsync(string), ungated for the same reason: both concern only the caller's own membership, which is not somebody else's information to protect. The check is the signature — there is no user key to point at anyone else. The refusals are enforced by the service, not merely hidden in the UI.
Removing another member remains ITeamManagementService.RemoveMemberAsync and still
requires member:manage.
A default interface method, so a host that substitutes or decorates this facet — which
TryAdd registration exists to allow — keeps compiling. It throws rather than no-opping:
reporting success for a departure that did not happen leaves someone believing they are out of a
team they are still in.