mod endpoints;
pub use endpoints::{
handle_accept_invitation, handle_cancel_invitation, handle_get_invitation,
handle_invite_member, handle_list_invitations, handle_list_user_invitations,
handle_reject_invitation,
};
use better_auth_core::entity::{AuthInvitation, AuthMember, AuthUser};
use better_auth_core::error::{AuthError, AuthResult};
use better_auth_core::plugin::AuthContext;
use better_auth_core::session::NativeSessionData;
use better_auth_core::types::{AuthRequest, AuthResponse, InvitationStatus};
use better_auth_core::wire::InvitationView;
use better_auth_core::{AuthRecordFields, FieldValue, SchemaValue};
use super::{require_native_session, resolve_organization_id};
use crate::plugins::organization::rbac::check_permission;
use crate::plugins::organization::types::{
AcceptInvitationRequest, AcceptInvitationResponse, BasicMemberResponse,
CancelInvitationRequest, GetInvitationQuery, GetInvitationResponse, InviteMemberRequest,
ListInvitationsQuery, RejectInvitationRequest, UserInvitationResponse,
};
use crate::plugins::organization::{InvitationEmail, OrganizationConfig, hooks::*};
fn normalized_roles(input: &crate::plugins::organization::types::RoleInput) -> String {
input.joined()
}
fn requested_roles(input: &crate::plugins::organization::types::RoleInput) -> Vec<&str> {
input.roles()
}
pub(crate) async fn invite_member_core(
body: &InviteMemberRequest,
session: &NativeSessionData,
config: &OrganizationConfig,
ctx: &AuthContext<impl better_auth_core::AuthSchema>,
request: Option<&AuthRequest>,
) -> AuthResult<InvitationView> {
let org_value = body.organization_id.field_value();
let org_value = if org_value.is_truthy() {
org_value
} else {
session.session_property("activeOrganizationId")?
};
let org_value = org_value.is_truthy().then_some(org_value).ok_or_else(|| {
AuthError::from_code(
"BAD_REQUEST",
"ORGANIZATION_NOT_FOUND",
"Organization not found",
)
})?;
let email = crate::plugins::organization::input::string_operation(
&body.email,
"invitation email.toLowerCase",
)?
.to_lowercase();
if !validator::ValidateEmail::validate_email(&email) {
return Err(AuthError::from_code(
"BAD_REQUEST",
"INVALID_EMAIL",
"Invalid email",
));
}
let resend = body.resend.is_truthy()?;
let member = ctx
.database
.get_member_with_user_value(&org_value, &session.user_property("id")?)
.await?
.map(|joined| joined.member)
.ok_or_else(|| {
AuthError::from_code("BAD_REQUEST", "MEMBER_NOT_FOUND", "Member not found")
})?;
let org_id = org_value;
if !check_permission(
member.role().typed()?,
&org_id,
"invitation",
&["create"],
config,
ctx,
)
.await?
{
return Err(AuthError::from_code(
"FORBIDDEN",
"YOU_ARE_NOT_ALLOWED_TO_INVITE_USERS_TO_THIS_ORGANIZATION",
"You are not allowed to invite users to this organization",
));
}
let normalized_role = crate::plugins::organization::input::parse_roles(&body.role)?;
let normalized_role = crate::plugins::organization::input::string_operation(
&normalized_role,
"invitation role.split",
)?;
let role_input =
crate::plugins::organization::types::RoleInput::One(normalized_role.to_owned());
let roles = requested_roles(&role_input);
let dynamic_roles = if config.dynamic_access_control {
ctx.database
.query_organization_roles_value(
&org_id,
&roles
.iter()
.map(|role| (*role).to_owned())
.collect::<Vec<_>>(),
)
.await?
} else {
Vec::new()
};
let mut valid_roles: Vec<&str> = vec!["owner", "admin", "member"];
valid_roles.extend(
config
.roles
.iter()
.flat_map(|roles| roles.keys())
.map(String::as_str),
);
for role in &dynamic_roles {
valid_roles.push(role.role.typed()?.as_str());
}
let unknown_roles: Vec<_> = roles
.iter()
.copied()
.filter(|role| !valid_roles.contains(role))
.collect();
if !unknown_roles.is_empty() {
return Err(AuthError::from_status(
"BAD_REQUEST",
better_auth_core::FieldMap::from([(
"message".into(),
format!("ROLE_NOT_FOUND: {}", unknown_roles.join(", ")).into(),
)])
.into(),
));
}
let member_is_creator = member
.role()
.typed()?
.split(',')
.map(str::trim)
.any(|role| role == config.creator_role);
let invites_creator_role = roles.iter().any(|role| *role == config.creator_role);
if invites_creator_role && !member_is_creator {
return Err(AuthError::from_code(
"FORBIDDEN",
"YOU_ARE_NOT_ALLOWED_TO_INVITE_USER_WITH_THIS_ROLE",
"You are not allowed to invite a user with this role",
));
}
if let Some(existing_user) = ctx.database.get_user_by_email(&email).await?
&& ctx
.database
.get_member_value(&org_id, &existing_user.id().field_value())
.await?
.is_some()
{
return Err(AuthError::from_code(
"BAD_REQUEST",
"USER_IS_ALREADY_A_MEMBER_OF_THIS_ORGANIZATION",
"User is already a member of this organization",
));
}
let existing = ctx
.database
.get_pending_invitation_value(&org_id, &email)
.await?;
if existing.is_some() && !resend && !config.cancel_pending_invitations_on_re_invite {
return Err(AuthError::from_code(
"BAD_REQUEST",
"USER_IS_ALREADY_INVITED_TO_THIS_ORGANIZATION",
"User is already invited to this organization",
));
}
let organization = ctx
.database
.get_organization_by_id_value(&org_id)
.await?
.ok_or_else(|| {
AuthError::from_code(
"BAD_REQUEST",
"ORGANIZATION_NOT_FOUND",
"Organization not found",
)
})?;
let organization_view = crate::plugins::organization::fields::organization(&organization, ctx)?;
let expires_at = config.invitation_expires_at(chrono::Utc::now())?;
let is_resend = existing.is_some() && resend;
let invitation = if let Some(existing) = existing.as_ref().filter(|_| resend) {
let _ = ctx
.database
.update_invitation_expiry_value(&existing.id().field_value(), expires_at)
.await?;
let mut response = existing.clone();
response.expires_at = expires_at.into();
response
} else {
if let Some(existing) = existing {
let _ = ctx
.database
.update_invitation_status_value(
&existing.id().field_value(),
InvitationStatus::Canceled,
)
.await?;
}
{
let limit = config
.pending_invitation_limit(
&member,
&organization_view,
|| session.user_value(),
OrganizationEndpoint::new(ctx, request),
)
.await?;
let count = ctx
.database
.count_pending_organization_invitations_value(&org_id)
.await? as usize;
if count >= limit {
return Err(AuthError::from_code(
"FORBIDDEN",
"INVITATION_LIMIT_REACHED",
"Invitation limit reached",
));
}
}
let team_ids_value = body.team_id.field_value();
let check_teams = config.teams.enabled && team_ids_value.is_truthy();
let team_ids = crate::plugins::organization::input::invitation_team_ids(team_ids_value);
if check_teams {
let raw = team_ids.field_value();
let requested = raw
.as_array()
.ok_or_else(|| AuthError::from(AuthResponse::new(500)))?;
for team_id in requested.iter()? {
let team_id = team_id?;
let reserved = match team_id {
better_auth_core::FieldValue::String(id) => id.contains(','),
better_auth_core::FieldValue::Array(ids) => {
ids.includes(&better_auth_core::FieldValue::from(","))?
}
_ => return Err(AuthResponse::new(500).into()),
};
if reserved {
return Err(AuthError::from_code(
"BAD_REQUEST",
"INVALID_TEAM_ID",
"Team id contains a reserved character",
));
}
}
for team_id in requested.iter()? {
let team_id = team_id?;
let _ = ctx
.database
.get_team_details_value(&team_id, Some(&org_id), false)
.await?
.ok_or_else(|| {
AuthError::from_code("BAD_REQUEST", "TEAM_NOT_FOUND", "Team not found")
})?
.into_public_parts(&config.schema.team)?;
}
if config.teams.maximum_members_per_team.is_some()
|| config.teams.maximum_members_per_team_callback.is_some()
{
for team_id in requested.iter()? {
let team_id = team_id?;
let team = ctx
.database
.get_team_details_value(&team_id, Some(&org_id), true)
.await?
.ok_or_else(|| {
AuthError::from_code("BAD_REQUEST", "TEAM_NOT_FOUND", "Team not found")
})?;
let (_, members) = team.into_public_parts(&config.schema.team)?;
let members = members.ok_or_else(|| {
AuthError::internal("Team membership projection was omitted")
})?;
let limit = config
.team_member_limit(OrganizationTeamMemberLimit {
organization_id: &org_id,
team_id: &team_id,
session,
})
.await?;
if let Some(limit) = limit
&& members.len() >= limit
{
return Err(AuthError::from_code(
"FORBIDDEN",
"TEAM_MEMBER_LIMIT_REACHED",
"Team member limit reached",
));
}
}
}
}
let mut draft = OrganizationInvitationDraft {
additional_fields: body.additional_fields.clone(),
team_id: crate::plugins::organization::input::invitation_team_alias(&team_ids)?,
id: body
.additional_fields
.get("id")
.and_then(better_auth_core::FieldValue::as_str)
.map(str::to_owned),
created_at: body
.additional_fields
.get("createdAt")
.cloned()
.map(better_auth_core::SchemaValue::Dynamic)
.unwrap_or_default(),
status: body
.additional_fields
.get("status")
.cloned()
.map(better_auth_core::SchemaValue::Dynamic)
.unwrap_or_default(),
organization_id: better_auth_core::SchemaValue::from_field(org_id),
email: email.clone(),
role: normalized_roles(&role_input),
inviter_id: better_auth_core::SchemaValue::from_field(session.user_property("id")?),
expires_at: body
.additional_fields
.get("expiresAt")
.cloned()
.map(better_auth_core::SchemaValue::Dynamic)
.unwrap_or_default(),
team_ids,
};
if let Some(hooks) = &config.hooks {
hooks
.before_create_invitation(
&mut draft,
OrganizationUser {
organization: &organization_view,
user: &session.user_value()?,
},
)
.await?;
}
let expires_at = config.invitation_expires_at(chrono::Utc::now())?;
ctx.database
.create_invitation(draft.into_create(expires_at, &session.user_property("id")?)?)
.await?
};
let invitation = crate::plugins::organization::fields::invitation_snapshot(invitation, config);
let invitation_view = InvitationView::try_from_invitation(&invitation)?;
let mut endpoint = crate::plugins::endpoint_context::EndpointContext::new(
request,
better_auth_core::FieldValue::from_json(serde_json::to_value(body)?)?,
ctx,
);
endpoint.session = Some(session.clone());
let task = crate::plugins::organization::callbacks::delivery(
config,
|| {
Ok(InvitationEmail {
invitation: invitation_view.clone(),
organization: organization_view.clone(),
member,
inviter: session.user_value()?,
request: endpoint.request.cloned(),
})
},
&endpoint,
)?;
better_auth_core::background::run_or_await(
task,
ctx.config.advanced.background_tasks.as_ref(),
ctx.logger(),
)
.await?;
if !is_resend && let Some(hooks) = &config.hooks {
hooks
.after_create_invitation(OrganizationInvitationEvent {
invitation: &invitation,
user: &session.user_value()?,
organization: &organization_view,
})
.await?;
}
Ok(invitation_view)
}
pub(crate) async fn get_invitation_core(
query: &GetInvitationQuery,
session: &NativeSessionData,
config: &OrganizationConfig,
ctx: &AuthContext<impl better_auth_core::AuthSchema>,
) -> AuthResult<Option<GetInvitationResponse<InvitationView>>> {
let Some(invitation) = ctx.database.get_invitation_by_id(&query.id).await? else {
return Ok(None);
};
if !invitation.is_pending() || invitation.is_expired()? {
return Ok(None);
}
let recipient = invitation.email().typed()?;
if recipient.to_lowercase()
!= crate::plugins::helpers::user_email_field(&session.user_property("email")?)?
.to_lowercase()
{
return Err(AuthError::from_code(
"FORBIDDEN",
"YOU_ARE_NOT_THE_RECIPIENT_OF_THE_INVITATION",
"You are not the recipient of the invitation",
));
}
if config.require_email_verification_on_invitation
&& !session.user_property("emailVerified")?.is_truthy()
{
return Err(AuthError::from_code(
"FORBIDDEN",
"EMAIL_VERIFICATION_REQUIRED_FOR_INVITATION",
"Email verification required to view or list invitations for the session email",
));
}
let organization = ctx
.database
.get_organization_by_id_value(&invitation.organization_id.field_value())
.await?
.ok_or_else(|| {
AuthError::from_code(
"BAD_REQUEST",
"ORGANIZATION_NOT_FOUND",
"Organization not found",
)
})?;
let organization_view = crate::plugins::organization::fields::organization(&organization, ctx)?;
let inviter = ctx
.database
.get_member_with_user_value(
&invitation.organization_id.field_value(),
&invitation.inviter_id.field_value(),
)
.await?
.ok_or_else(|| {
AuthError::from_code(
"BAD_REQUEST",
"INVITER_IS_NO_LONGER_A_MEMBER_OF_THE_ORGANIZATION",
"Inviter is no longer a member of the organization",
)
})?;
let inviter_email = inviter.user.email;
Ok(Some(GetInvitationResponse {
invitation: InvitationView::try_from_invitation(&invitation)?,
organization_name: organization_view.name,
organization_slug: organization_view.slug,
inviter_email,
}))
}
pub(crate) async fn list_invitations_core(
query: &ListInvitationsQuery,
session: &NativeSessionData,
ctx: &AuthContext<impl better_auth_core::AuthSchema>,
) -> AuthResult<Vec<InvitationView>> {
let org_id = resolve_organization_id(
query.organization_id.as_deref(),
session,
AuthError::from_status(
"BAD_REQUEST",
better_auth_core::FieldMap::from([(
"message".into(),
"Organization ID is required".into(),
)])
.into(),
),
)?;
let _ = ctx
.database
.get_member_with_user_value(&org_id, &session.user_property("id")?)
.await?
.ok_or_else(|| {
AuthError::from_status(
"FORBIDDEN",
better_auth_core::FieldMap::from([(
"message".into(),
"You are not a member of this organization".into(),
)])
.into(),
)
})?;
let invitations = ctx
.database
.list_organization_invitations_value(&org_id)
.await?;
invitations
.iter()
.map(InvitationView::try_from_invitation)
.collect()
}
pub(crate) async fn list_user_invitations_core(
session: Option<&NativeSessionData>,
email: Option<&str>,
ctx: &AuthContext<impl better_auth_core::AuthSchema>,
) -> AuthResult<Vec<UserInvitationResponse<InvitationView>>> {
if let Some(session) = session
&& !session.user_property("emailVerified")?.is_truthy()
{
return Err(AuthError::from_code(
"FORBIDDEN",
"EMAIL_VERIFICATION_REQUIRED_FOR_INVITATION",
"Email verification required to view or list invitations for the session email",
));
}
let email = match session
.map(|session| session.user_property("email"))
.transpose()?
.filter(|email| email.is_truthy())
{
Some(email) => email,
None => email
.map(better_auth_core::FieldValue::from)
.unwrap_or_default(),
};
if !email.is_truthy() {
return Err(AuthError::from_status(
"BAD_REQUEST",
better_auth_core::FieldMap::from([(
"message".into(),
"Missing session headers, or email query parameter.".into(),
)])
.into(),
));
}
let user_email = crate::plugins::helpers::user_email_field(&email)?;
let all_invitations = ctx.database.list_user_invitations(&user_email).await?;
let pending = all_invitations
.into_iter()
.filter(|row| row.invitation.status == InvitationStatus::Pending)
.map(|row| {
Ok(UserInvitationResponse {
invitation: InvitationView::try_from_invitation(&row.invitation)?,
organization_name: SchemaValue::from_field(match row.organization {
better_auth_core::store::JoinValue::One(Some(organization)) => {
organization.get("name").cloned().unwrap_or_default()
}
_ => FieldValue::Undefined,
}),
})
})
.collect::<AuthResult<Vec<_>>>()?;
Ok(pending)
}
pub(crate) async fn accept_invitation_core(
body: &AcceptInvitationRequest,
session: &NativeSessionData,
config: &OrganizationConfig,
ctx: &AuthContext<impl better_auth_core::AuthSchema>,
request: &AuthRequest,
) -> AuthResult<AcceptInvitationResponse<InvitationView, BasicMemberResponse>> {
let invitation = ctx
.database
.get_invitation_by_id(&body.invitation_id)
.await?
.ok_or_else(|| {
AuthError::from_code(
"BAD_REQUEST",
"INVITATION_NOT_FOUND",
"Invitation not found",
)
})?;
if !invitation.is_pending() || invitation.is_expired()? {
return Err(AuthError::from_code(
"BAD_REQUEST",
"INVITATION_NOT_FOUND",
"Invitation not found",
));
}
let recipient = crate::plugins::helpers::user_email_field(&invitation.email().field_value())?;
let user_email = crate::plugins::helpers::user_email_field(&session.user_property("email")?)?;
if recipient.to_lowercase() != user_email.to_lowercase() {
return Err(AuthError::from_code(
"FORBIDDEN",
"YOU_ARE_NOT_THE_RECIPIENT_OF_THE_INVITATION",
"You are not the recipient of the invitation",
));
}
if config.require_email_verification_on_invitation
&& !session.user_property("emailVerified")?.is_truthy()
{
return Err(AuthError::from_code(
"FORBIDDEN",
"EMAIL_VERIFICATION_REQUIRED_BEFORE_ACCEPTING_OR_REJECTING_INVITATION",
"Email verification required before accepting or rejecting invitation",
));
}
let count = ctx
.database
.count_organization_members_value(&invitation.organization_id.field_value())
.await?;
let organization = ctx
.database
.get_organization_by_id_value(&invitation.organization_id.field_value())
.await?
.ok_or_else(|| {
AuthError::from_code(
"BAD_REQUEST",
"ORGANIZATION_NOT_FOUND",
"Organization not found",
)
})?;
let organization_view = crate::plugins::organization::fields::organization(&organization, ctx)?;
let limit = config
.member_limit(&organization_view, || session.user_value())
.await?;
if count >= limit as i64 {
return Err(AuthError::from_code(
"FORBIDDEN",
"ORGANIZATION_MEMBERSHIP_LIMIT_REACHED",
"Organization membership limit reached",
));
}
if let Some(hooks) = &config.hooks {
hooks
.before_accept_invitation(OrganizationInvitationEvent {
invitation: &invitation,
user: &session.user_value()?,
organization: &organization_view,
})
.await?;
}
let resolver = InvitationTeamLimits { config, session };
let manager = ctx.session_manager();
let request = request.clone();
let session_source = session.clone();
let (member, accepted, _) = ctx
.database
.accept_invitation_with_session_effects_values(
&body.invitation_id.as_str().into(),
&session.user_property("id")?,
Some(&session.session_property("token")?),
config.teams.enabled,
better_auth_core::store::TeamMemberLimits::Resolver(&resolver),
better_auth_core::store::SessionWriteEffects {
secondary: None,
after_output: Some(Box::new(move |session, transaction| {
Box::pin(async move {
manager
.set_native_session_cookie_in_transaction(
&request,
session_source.with_session(session)?,
None,
transaction.as_ref(),
)
.await
})
})),
},
)
.await?;
if let Some(hooks) = &config.hooks {
hooks
.after_accept_invitation(
&member,
OrganizationInvitationEvent {
invitation: &accepted,
user: &session.user_value()?,
organization: &organization_view,
},
)
.await?;
}
Ok(AcceptInvitationResponse {
invitation: InvitationView::try_from_invitation(&accepted)?,
member: BasicMemberResponse::try_from_member(&member)?,
})
}
pub(crate) async fn reject_invitation_core(
body: &RejectInvitationRequest,
session: &NativeSessionData,
config: &OrganizationConfig,
ctx: &AuthContext<impl better_auth_core::AuthSchema>,
) -> AuthResult<AcceptInvitationResponse<Option<InvitationView>, Option<BasicMemberResponse>>> {
let invitation = ctx
.database
.get_invitation_by_id(&body.invitation_id)
.await?
.filter(|invitation| invitation.is_pending())
.ok_or_else(|| {
AuthError::from_code(
"BAD_REQUEST",
"INVITATION_NOT_FOUND",
"Invitation not found!",
)
})?;
let recipient = crate::plugins::helpers::user_email_field(&invitation.email().field_value())?;
let user_email = crate::plugins::helpers::user_email_field(&session.user_property("email")?)?;
if recipient.to_lowercase() != user_email.to_lowercase() {
return Err(AuthError::from_code(
"FORBIDDEN",
"YOU_ARE_NOT_THE_RECIPIENT_OF_THE_INVITATION",
"You are not the recipient of the invitation",
));
}
if config.require_email_verification_on_invitation
&& !session.user_property("emailVerified")?.is_truthy()
{
return Err(AuthError::from_code(
"FORBIDDEN",
"EMAIL_VERIFICATION_REQUIRED_BEFORE_ACCEPTING_OR_REJECTING_INVITATION",
"Email verification required before accepting or rejecting invitation",
));
}
let organization = ctx
.database
.get_organization_by_id_value(&invitation.organization_id.field_value())
.await?
.ok_or_else(|| {
AuthError::from_code(
"BAD_REQUEST",
"ORGANIZATION_NOT_FOUND",
"Organization not found",
)
})?;
let organization_view = crate::plugins::organization::fields::organization(&organization, ctx)?;
if let Some(hooks) = &config.hooks {
hooks
.before_reject_invitation(OrganizationInvitationEvent {
invitation: &invitation,
user: &session.user_value()?,
organization: &organization_view,
})
.await?;
}
let updated_invitation = ctx
.database
.update_invitation_status(&body.invitation_id, InvitationStatus::Rejected)
.await?;
if let Some(hooks) = &config.hooks {
hooks
.after_reject_invitation(OrganizationInvitationEvent {
invitation: updated_invitation.as_ref().unwrap_or(&invitation),
user: &session.user_value()?,
organization: &organization_view,
})
.await?;
}
Ok(AcceptInvitationResponse {
invitation: updated_invitation
.as_ref()
.map(InvitationView::try_from_invitation)
.transpose()?,
member: None,
})
}
pub(crate) async fn cancel_invitation_core(
body: &CancelInvitationRequest,
session: &NativeSessionData,
config: &OrganizationConfig,
ctx: &AuthContext<impl better_auth_core::AuthSchema>,
) -> AuthResult<Option<InvitationView>> {
let invitation = ctx
.database
.get_invitation_by_id(&body.invitation_id)
.await?
.ok_or_else(|| {
AuthError::from_code(
"BAD_REQUEST",
"INVITATION_NOT_FOUND",
"Invitation not found",
)
})?;
let member = ctx
.database
.get_member_with_user_value(
&invitation.organization_id().field_value(),
&session.user_property("id")?,
)
.await?
.map(|joined| joined.member)
.ok_or_else(|| {
AuthError::from_code("BAD_REQUEST", "MEMBER_NOT_FOUND", "Member not found")
})?;
if !check_permission(
member.role().typed()?,
&invitation.organization_id().field_value(),
"invitation",
&["cancel"],
config,
ctx,
)
.await?
{
return Err(AuthError::from_code(
"FORBIDDEN",
"YOU_ARE_NOT_ALLOWED_TO_CANCEL_THIS_INVITATION",
"You are not allowed to cancel this invitation",
));
}
let organization = ctx
.database
.get_organization_by_id_value(&invitation.organization_id.field_value())
.await?
.ok_or_else(|| {
AuthError::from_code(
"BAD_REQUEST",
"ORGANIZATION_NOT_FOUND",
"Organization not found",
)
})?;
let organization_view = crate::plugins::organization::fields::organization(&organization, ctx)?;
if let Some(hooks) = &config.hooks {
hooks
.before_cancel_invitation(OrganizationInvitationEvent {
invitation: &invitation,
user: &session.user_value()?,
organization: &organization_view,
})
.await?;
}
let updated_invitation = ctx
.database
.update_invitation_status(&body.invitation_id, InvitationStatus::Canceled)
.await?;
if let Some(hooks) = &config.hooks {
hooks
.after_cancel_invitation(OrganizationInvitationEvent {
invitation: updated_invitation.as_ref().unwrap_or(&invitation),
user: &session.user_value()?,
organization: &organization_view,
})
.await?;
}
updated_invitation
.as_ref()
.map(InvitationView::try_from_invitation)
.transpose()
}
#[cfg(test)]
mod tests;
struct InvitationTeamLimits<'a> {
config: &'a OrganizationConfig,
session: OrganizationSession<'a>,
}
#[async_trait::async_trait]
impl better_auth_core::store::TeamMemberLimitResolver for InvitationTeamLimits<'_> {
fn team_output_fields(&self) -> Option<&better_auth_core::user_fields::UserConfig> {
Some(&self.config.schema.team)
}
async fn maximum(
&self,
team_id: &str,
organization_id: &better_auth_core::FieldValue,
) -> AuthResult<Option<usize>> {
self.config
.team_member_limit(OrganizationTeamMemberLimit {
organization_id,
team_id: &team_id.into(),
session: self.session,
})
.await
}
}