Interface ITeamDirectoryService
- Namespace
- Tharga.Team
- Assembly
- Tharga.Team.dll
The caller's own teams and their own standing in them — the interface a component, controller or MCP
provider should inject to list those teams, ask whether their membership in one is suspended, read
their own access to it, or leave it. The listing is scope-filtered rather than scope-gated: each
team is included only if the caller's membership in that team grants team:read. The rest is
ungated, because it concerns only the caller's own membership.
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. The listing here names no team, so it cannot be gated the same way, and the operations that do name one ask only about the caller's own membership, which is not somebody else's information to protect. Neither belongs 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
GetMyAccessAsync(string)
The caller's own access to this team — through membership, or through the team's consent to a role they hold — or null when they have neither, or are a suspended member.
Task<CallerTeamAccess> GetMyAccessAsync(string teamKey)
Parameters
teamKeystring
Returns
Remarks
Ungated, beside IsSuspendedAsync(string) and for the same reason: it concerns only the caller's own standing, and there is no user key to point at anyone else. It answers with the rule that issues the caller's claims, so a list labelled with it agrees with what selecting the team then grants.
A default interface method returning null, so a host that substitutes or decorates this facet keeps compiling; null reads as "no access shown", which hides a label rather than inventing one.
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.