better-auth-api 1.0.0-alpha.5

Plugin implementations for better-auth
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
//! Application hooks preserve native callback subjects and upstream `data` overrides.

use super::types::OrganizationResponse;

use async_trait::async_trait;
use better_auth_core::{
    AuthConfig, AuthContext, AuthRequest, AuthResult, AuthSchema, CreateInvitation, CreateMember,
    CreateOrganization, CreateTeam, FieldValue, Invitation, Member, Team, TeamMember,
    UpdateOrganization, UpdateTeam,
    session::NativeSessionData,
    store::{InvitationStore, MemberStore, OrganizationStore, TeamStore},
};

/// Request and persistence capabilities supplied to endpoint-aware callbacks.
pub struct OrganizationEndpoint<'a> {
    /// HTTP request, absent for a trusted server call without a request.
    pub request: Option<&'a AuthRequest>,
    /// Global authentication configuration.
    pub auth_config: &'a AuthConfig,
    /// Organization persistence used by the running auth instance.
    pub organizations: &'a dyn OrganizationStore,
    /// Team persistence, including atomic member capacity checks.
    pub teams: &'a dyn TeamStore,
    /// Organization membership persistence.
    pub members: &'a dyn MemberStore,
    /// Invitation persistence.
    pub invitations: &'a dyn InvitationStore,
}

impl<'a> OrganizationEndpoint<'a> {
    pub(crate) fn new<S: AuthSchema>(
        ctx: &'a AuthContext<S>,
        request: Option<&'a AuthRequest>,
    ) -> Self {
        Self {
            request,
            auth_config: &ctx.config,
            organizations: ctx.database.as_ref(),
            teams: ctx.database.as_ref(),
            members: ctx.database.as_ref(),
            invitations: ctx.database.as_ref(),
        }
    }
}

/// An organization and the user acting on it.
#[derive(Clone, Copy)]
pub struct OrganizationUser<'a> {
    /// Persisted organization associated with the operation.
    pub organization: &'a OrganizationResponse,
    /// Acting user, or the invited/added user where upstream supplies that user.
    pub user: &'a FieldValue,
}

/// Member and user supplied to organization update hooks.
#[derive(Clone, Copy)]
pub struct OrganizationActor<'a> {
    /// Acting user's organization membership.
    pub member: &'a Member,
    /// Acting user.
    pub user: &'a FieldValue,
}

/// A membership operation and its upstream user subject.
#[derive(Clone, Copy)]
pub struct OrganizationMemberEvent<'a> {
    /// Membership being added, removed, or changed.
    pub member: &'a Member,
    /// User supplied by the upstream hook at this call site.
    pub user: &'a FieldValue,
    /// Organization containing the membership.
    pub organization: &'a OrganizationResponse,
}

/// Invitation state and the user acting on it.
#[derive(Clone, Copy)]
pub struct OrganizationInvitationEvent<'a> {
    /// Invitation snapshot at this hook's position in the operation.
    pub invitation: &'a Invitation,
    /// Inviter, recipient, or cancelling user, according to the hook.
    pub user: &'a FieldValue,
    /// Organization receiving the invited user.
    pub organization: &'a OrganizationResponse,
}

/// Team state and its optional authenticated actor.
#[derive(Clone, Copy)]
pub struct OrganizationTeamEvent<'a> {
    /// Persisted team snapshot.
    pub team: &'a Team,
    /// Acting user; absent for trusted team creation/deletion without a session.
    pub user: Option<&'a FieldValue>,
    /// Organization containing the team.
    pub organization: &'a OrganizationResponse,
}

/// The target of a team membership change.
#[derive(Clone, Copy)]
pub struct OrganizationTeamMemberTarget<'a> {
    /// Team receiving or losing the member.
    pub team: &'a Team,
    /// User being added or removed, rather than the administrator.
    pub user: &'a FieldValue,
    /// Organization containing the team.
    pub organization: &'a OrganizationResponse,
}

/// Membership input before the adapter generates its creation timestamp.
#[derive(Debug, Clone)]
pub struct OrganizationMemberDraft {
    pub additional_fields: better_auth_core::FieldMap,

    pub organization_id: better_auth_core::SchemaValue<String>,
    pub user_id: better_auth_core::SchemaValue<String>,
    pub role: better_auth_core::SchemaValue<String>,
    /// Server-side addMember input; team assignment is handled separately from this record.
    pub team_id: better_auth_core::SchemaValue<String>,
    /// The hook starts without a timestamp; the adapter always generates the stored value.
    pub created_at: Option<better_auth_core::FieldDate>,
}
impl OrganizationMemberDraft {
    pub(crate) fn into_create(mut self) -> CreateMember {
        if !self.team_id.is_undefined() {
            let _ = self
                .additional_fields
                .insert("teamId".into(), self.team_id.field_value());
        }
        CreateMember {
            organization_id: self.organization_id,
            user_id: self.user_id,
            role: self.role,
            additional_fields: self.additional_fields,
        }
    }
}

/// Team fields visible before adapter defaults are applied.
#[derive(Debug, Clone)]
pub struct OrganizationTeamDraft {
    pub additional_fields: better_auth_core::FieldMap,

    pub id: Option<String>,
    pub name: better_auth_core::SchemaValue<String>,
    pub organization_id: better_auth_core::SchemaValue<String>,
    pub created_at: Option<better_auth_core::FieldDate>,
    /// None leaves the route default; Some(None) explicitly clears the timestamp.
    pub updated_at: Option<Option<better_auth_core::FieldDate>>,
}
impl OrganizationTeamDraft {
    pub(crate) fn into_create(
        mut self,
        created_at: chrono::DateTime<chrono::Utc>,
        updated_at: Option<chrono::DateTime<chrono::Utc>>,
    ) -> CreateTeam {
        if self.created_at.is_some() {
            let _ = self.additional_fields.remove("createdAt");
        }
        if self.updated_at.is_some() {
            let _ = self.additional_fields.remove("updatedAt");
        }
        CreateTeam {
            additional_fields: self.additional_fields,
            id: self.id,
            name: self.name,
            organization_id: self.organization_id,
            created_at: Some(self.created_at.unwrap_or_else(|| created_at.into())),
            updated_at: self
                .updated_at
                .unwrap_or_else(|| updated_at.map(Into::into)),
        }
    }
}

/// Invitation fields visible before adapter defaults are applied.
#[derive(Debug, Clone)]
pub struct OrganizationInvitationDraft {
    pub additional_fields: better_auth_core::FieldMap,

    pub id: Option<String>,
    pub organization_id: better_auth_core::SchemaValue<String>,
    pub email: String,
    pub role: String,
    pub inviter_id: better_auth_core::SchemaValue<String>,
    pub created_at: better_auth_core::SchemaValue<better_auth_core::FieldDate>,
    pub status: better_auth_core::SchemaValue<better_auth_core::InvitationStatus>,
    pub expires_at: better_auth_core::SchemaValue<better_auth_core::FieldDate>,
    /// Informational first-team alias. Upstream ignores returned changes to this field.
    pub team_id: better_auth_core::SchemaValue<String>,
    /// Destination teams used by the adapter after the hook.
    pub team_ids: better_auth_core::SchemaValue<Vec<String>>,
}
impl OrganizationInvitationDraft {
    pub(crate) fn into_create(
        mut self,
        expires_at: chrono::DateTime<chrono::Utc>,
        inviter_id: &FieldValue,
    ) -> AuthResult<CreateInvitation> {
        for (name, value) in [
            ("createdAt", self.created_at.field_value()),
            ("status", self.status.field_value()),
            ("expiresAt", self.expires_at.field_value()),
        ] {
            if !value.is_undefined() {
                let _ = self.additional_fields.insert(name.into(), value);
            }
        }
        if !self.inviter_id.field_value().strict_equals(inviter_id) {
            let _ = self.additional_fields.remove("inviterId");
        }
        Ok(CreateInvitation {
            additional_fields: self.additional_fields,
            id: self.id,
            created_at: None,
            status: None,
            organization_id: self.organization_id,
            email: self.email,
            role: self.role,
            inviter_id: self.inviter_id,
            expires_at: expires_at.into(),
            team_id: super::input::join_invitation_teams(&self.team_ids)?,
        })
    }
}

/// Hooks run in upstream order. Returning an error stops the current operation.
///
/// Before hooks modify typed inputs in place. After-hook failures do not undo
/// persistence that upstream has already committed.
#[async_trait]
pub trait OrganizationHooks: Send + Sync {
    /// Override organization creation fields after the initial slug check.
    async fn before_create_organization(
        &self,
        _data: &mut CreateOrganization,
        _user: &FieldValue,
    ) -> AuthResult<()> {
        Ok(())
    }
    /// Run after owner/default-team creation and before selecting the active organization.
    async fn after_create_organization(
        &self,
        _event: OrganizationMemberEvent<'_>,
    ) -> AuthResult<()> {
        Ok(())
    }
    /// Override the update after permission and initial slug checks.
    async fn before_update_organization(
        &self,
        _data: &mut UpdateOrganization,
        _actor: OrganizationActor<'_>,
    ) -> AuthResult<()> {
        Ok(())
    }
    /// Observe the persisted organization update.
    async fn after_update_organization(
        &self,
        _organization: &OrganizationResponse,
        _actor: OrganizationActor<'_>,
    ) -> AuthResult<()> {
        Ok(())
    }
    /// Run before deleting organization data.
    async fn before_delete_organization(
        &self,
        _event: OrganizationUser<'_>,
        _ctx: OrganizationEndpoint<'_>,
    ) -> AuthResult<()> {
        Ok(())
    }
    /// Run after deleting organization data.
    async fn after_delete_organization(
        &self,
        _event: OrganizationUser<'_>,
        _ctx: OrganizationEndpoint<'_>,
    ) -> AuthResult<()> {
        Ok(())
    }
    /// Override membership creation fields for an owner or server-side member addition.
    async fn before_add_member(
        &self,
        _data: &mut OrganizationMemberDraft,
        _event: OrganizationUser<'_>,
    ) -> AuthResult<()> {
        Ok(())
    }
    /// Observe membership creation after any requested team membership succeeds.
    async fn after_add_member(&self, _event: OrganizationMemberEvent<'_>) -> AuthResult<()> {
        Ok(())
    }
    /// Run before removing a member. Leaving an organization does not invoke this hook.
    async fn before_remove_member(&self, _event: OrganizationMemberEvent<'_>) -> AuthResult<()> {
        Ok(())
    }
    /// Run after removing the member and clearing the applicable active organization.
    async fn after_remove_member(&self, _event: OrganizationMemberEvent<'_>) -> AuthResult<()> {
        Ok(())
    }
    /// Override the new role after validating the requested role and owner constraints.
    async fn before_update_member_role(
        &self,
        _role: &mut String,
        _event: OrganizationMemberEvent<'_>,
    ) -> AuthResult<()> {
        Ok(())
    }
    /// Observe the updated membership and the role it replaced.
    async fn after_update_member_role(
        &self,
        _previous_role: &str,
        _event: OrganizationMemberEvent<'_>,
    ) -> AuthResult<()> {
        Ok(())
    }
    /// Override invitation creation fields after permission and capacity checks.
    async fn before_create_invitation(
        &self,
        _data: &mut OrganizationInvitationDraft,
        _event: OrganizationUser<'_>,
    ) -> AuthResult<()> {
        Ok(())
    }
    /// Run after persisting a new invitation and delivering its email. Resends skip this hook.
    async fn after_create_invitation(
        &self,
        _event: OrganizationInvitationEvent<'_>,
    ) -> AuthResult<()> {
        Ok(())
    }
    /// Run after checking recipient membership capacity and before accepting the invitation.
    async fn before_accept_invitation(
        &self,
        _event: OrganizationInvitationEvent<'_>,
    ) -> AuthResult<()> {
        Ok(())
    }
    /// Observe the accepted invitation and its new organization membership.
    async fn after_accept_invitation(
        &self,
        _member: &Member,
        _event: OrganizationInvitationEvent<'_>,
    ) -> AuthResult<()> {
        Ok(())
    }
    /// Run before changing a recipient's invitation to rejected.
    async fn before_reject_invitation(
        &self,
        _event: OrganizationInvitationEvent<'_>,
    ) -> AuthResult<()> {
        Ok(())
    }
    /// Observe the rejected invitation.
    async fn after_reject_invitation(
        &self,
        _event: OrganizationInvitationEvent<'_>,
    ) -> AuthResult<()> {
        Ok(())
    }
    /// Run before cancellation; the event user is the cancelling user.
    async fn before_cancel_invitation(
        &self,
        _event: OrganizationInvitationEvent<'_>,
    ) -> AuthResult<()> {
        Ok(())
    }
    /// Observe the cancelled invitation.
    async fn after_cancel_invitation(
        &self,
        _event: OrganizationInvitationEvent<'_>,
    ) -> AuthResult<()> {
        Ok(())
    }
    /// Override new team fields, including the default team during organization creation.
    async fn before_create_team(
        &self,
        _data: &mut OrganizationTeamDraft,
        _organization: &OrganizationResponse,
        _user: Option<&FieldValue>,
    ) -> AuthResult<()> {
        Ok(())
    }
    /// Observe a persisted team after default-team membership, when applicable.
    async fn after_create_team(&self, _event: OrganizationTeamEvent<'_>) -> AuthResult<()> {
        Ok(())
    }
    /// Modify or replace the update after permission checks. Team IDs remain immutable.
    async fn before_update_team(
        &self,
        _updates: &mut UpdateTeam,
        _event: OrganizationTeamEvent<'_>,
    ) -> AuthResult<()> {
        Ok(())
    }
    /// Observe the persisted team update.
    async fn after_update_team(&self, _event: OrganizationTeamEvent<'_>) -> AuthResult<()> {
        Ok(())
    }
    /// Run before deleting team membership and invitation references.
    async fn before_delete_team(&self, _event: OrganizationTeamEvent<'_>) -> AuthResult<()> {
        Ok(())
    }
    /// Run after team deletion commits.
    async fn after_delete_team(&self, _event: OrganizationTeamEvent<'_>) -> AuthResult<()> {
        Ok(())
    }
    /// Run before adding a team member. Upstream ignores this hook's data overrides.
    async fn before_add_team_member(
        &self,
        _target: OrganizationTeamMemberTarget<'_>,
    ) -> AuthResult<()> {
        Ok(())
    }
    /// Observe the existing or newly created team membership.
    async fn after_add_team_member(
        &self,
        _member: &TeamMember,
        _target: OrganizationTeamMemberTarget<'_>,
    ) -> AuthResult<()> {
        Ok(())
    }
    /// Run before removing the target team membership.
    async fn before_remove_team_member(
        &self,
        _member: &TeamMember,
        _target: OrganizationTeamMemberTarget<'_>,
    ) -> AuthResult<()> {
        Ok(())
    }
    /// Run after removing the target team membership.
    async fn after_remove_team_member(
        &self,
        _member: &TeamMember,
        _target: OrganizationTeamMemberTarget<'_>,
    ) -> AuthResult<()> {
        Ok(())
    }
}

/// Native authenticated Session root supplied to dynamic team limits.
pub type OrganizationSession<'a> = &'a NativeSessionData;

/// Context for the maximum-team callback.
#[derive(Clone, Copy)]
pub struct OrganizationTeamLimit<'a> {
    /// Organization whose teams will be counted.
    pub organization_id: &'a better_auth_core::FieldValue,
    /// Current session, absent for trusted server-side team creation.
    pub session: Option<OrganizationSession<'a>>,
}

/// Context for atomic team membership capacity checks.
#[derive(Clone, Copy)]
pub struct OrganizationTeamMemberLimit<'a> {
    /// Organization containing the destination team.
    pub organization_id: &'a better_auth_core::FieldValue,
    /// Destination team identifier.
    pub team_id: &'a better_auth_core::FieldValue,
    /// Authenticated actor's session.
    pub session: OrganizationSession<'a>,
}

/// Asynchronous configuration callbacks. `None` keeps the configured static value.
#[async_trait]
pub trait OrganizationPolicy: Send + Sync {
    /// Decide whether this user may create an organization.
    async fn allow_user_to_create_organization(
        &self,
        _user: &FieldValue,
    ) -> AuthResult<Option<bool>> {
        Ok(None)
    }
    /// Return true when the user's organization limit has been reached.
    async fn organization_limit_reached(&self, _user: &FieldValue) -> AuthResult<Option<bool>> {
        Ok(None)
    }
    /// Resolve pending invitation capacity for the authenticated inviter.
    async fn invitation_limit(
        &self,
        _event: OrganizationMemberEvent<'_>,
        _ctx: OrganizationEndpoint<'_>,
    ) -> AuthResult<Option<usize>> {
        Ok(None)
    }
    /// Resolve the maximum number of teams before team creation.
    async fn maximum_teams(
        &self,
        _data: OrganizationTeamLimit<'_>,
        _ctx: OrganizationEndpoint<'_>,
    ) -> AuthResult<Option<usize>> {
        Ok(None)
    }
    /// Resolve the maximum number of stored dynamic roles.
    async fn maximum_roles_per_organization(
        &self,
        _organization_id: &better_auth_core::FieldValue,
    ) -> AuthResult<Option<usize>> {
        Ok(None)
    }
    /// Create and persist a default team, or return `None` for normal creation.
    async fn create_default_team(
        &self,
        _organization: &OrganizationResponse,
        _ctx: OrganizationEndpoint<'_>,
    ) -> AuthResult<Option<Team>> {
        Ok(None)
    }
}

/// Dynamic team capacity requires an authenticated actor session.
#[async_trait]
pub trait TeamMemberLimitPolicy: Send + Sync {
    async fn maximum_members_per_team(
        &self,
        data: OrganizationTeamMemberLimit<'_>,
    ) -> AuthResult<usize>;
}

/// Dynamic organization capacity. Listing endpoints still use the upstream default page size.
#[async_trait]
pub trait MembershipLimitPolicy: Send + Sync {
    async fn membership_limit(&self, data: OrganizationUser<'_>) -> AuthResult<usize>;
}