Table of Contents

Interface ITeamDirectoryService

Namespace
Tharga.Team
Assembly
Tharga.Team.dll

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

IAsyncEnumerable<ITeam>

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

teamKey string

Returns

Task<bool>

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

teamKey string

Returns

Task

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.