Skip to main content

alibi_core/store/contracts/
transaction.rs

1use crate::AdapterRecord;
2use crate::field_policy::FieldValues;
3use crate::types::AddTeamMemberResult;
4use crate::user_validation::{PreparedUserCreation, UserValidationSource};
5use crate::verification::{VerificationCreation, VerificationPublication, VerificationSnapshot};
6use crate::{
7    AuthError, AuthResult, AuthSchema, AuthStore, CreateAccount, CreateJwk, CreateMember,
8    CreatePasskey, CreateSession, CreateUser, CreateVerification, Jwk, Member, Passkey, Team,
9};
10use async_trait::async_trait;
11use std::any::Any;
12use std::pin::Pin;
13pub type BoxedTransactionValue = Box<dyn Any + Send>;
14
15pub type TransactionFuture<'a> =
16    Pin<Box<dyn Future<Output = AuthResult<BoxedTransactionValue>> + Send + 'a>>;
17
18pub type TypedTransactionFuture<'a, T> = Pin<Box<dyn Future<Output = AuthResult<T>> + Send + 'a>>;
19
20pub type TransactionWork<S> =
21    dyn for<'tx> FnOnce(&'tx dyn AuthTransaction<S>) -> TransactionFuture<'tx> + Send;
22
23#[async_trait]
24pub trait AuthTransaction<S: AuthSchema>: Send + Sync {
25    /// Read the provider verification column using the adapter's physical scalar
26    /// rules. This is retained output, not authorization input; typed models
27    /// continue to supply the canonical boolean accessor.
28    async fn provider_verification_output(
29        &self,
30        _id: &str,
31    ) -> AuthResult<Option<serde_json::Value>> {
32        Ok(None)
33    }
34
35    /// Stage a session using the actual transaction-local model and creation hooks.
36    async fn prepare_secondary_session_creation(
37        &self,
38        _input: CreateSession,
39        _persist: bool,
40    ) -> AuthResult<S::Session> {
41        Err(AuthError::NotImplemented(
42            "Transactional secondary session creation is unsupported".into(),
43        ))
44    }
45
46    /// Stage an update against a trusted cached model using this transaction's hooks.
47    async fn prepare_secondary_session_update(
48        &self,
49        _session: S::Session,
50        _expires_at: Option<chrono::DateTime<chrono::Utc>>,
51        _fields: FieldValues,
52    ) -> AuthResult<Option<(S::Session, FieldValues)>> {
53        Err(AuthError::NotImplemented(
54            "Transactional secondary session updates are unsupported".into(),
55        ))
56    }
57    /// Persist a staged update on the current connection; after hooks belong to commit.
58    async fn complete_secondary_session_update(
59        &self,
60        _session: S::Session,
61        _expires_at: Option<chrono::DateTime<chrono::Utc>>,
62        _fields: FieldValues,
63        _persist: bool,
64    ) -> AuthResult<Option<S::Session>> {
65        Err(AuthError::NotImplemented(
66            "Transactional secondary session updates are unsupported".into(),
67        ))
68    }
69
70    /// Read the managed signing keyring on this transaction's connection.
71    async fn list_jwks(&self) -> AuthResult<Vec<Jwk>> {
72        Err(AuthError::config(
73            "Transactional JWKS reads are unsupported by this store",
74        ))
75    }
76    /// Find a managed signing key on this transaction's connection.
77    async fn get_jwk_by_id(&self, _id: &str) -> AuthResult<Option<Jwk>> {
78        Err(AuthError::config(
79            "Transactional JWKS reads are unsupported by this store",
80        ))
81    }
82    /// Persist a newly generated managed signing key in this transaction.
83    async fn create_jwk(&self, _data: CreateJwk) -> AuthResult<Jwk> {
84        Err(AuthError::config(
85            "Transactional JWKS writes are unsupported by this store",
86        ))
87    }
88
89    /// Return a retained adapter record. The default is the physical model's
90    /// serialized snapshot; initialized stores apply their declared output policy.
91    async fn create_user_record(
92        &self,
93        create_user: CreateUser,
94    ) -> AuthResult<AdapterRecord<S::User>> {
95        AdapterRecord::physical(self.create_user(create_user).await?)
96    }
97
98    /// Return a retained adapter record. The default is the physical model's
99    /// serialized snapshot; initialized stores apply their declared output policy.
100    async fn create_user_with_source_record(
101        &self,
102        create_user: CreateUser,
103        source: UserValidationSource,
104    ) -> AuthResult<AdapterRecord<S::User>> {
105        AdapterRecord::physical(self.create_user_with_source(create_user, source).await?)
106    }
107
108    /// Return a retained adapter record. The default is the physical model's
109    /// serialized snapshot; initialized stores apply their declared output policy.
110    async fn create_user_prepared_record(
111        &self,
112        prepared: PreparedUserCreation,
113    ) -> AuthResult<AdapterRecord<S::User>> {
114        AdapterRecord::physical(self.create_user_prepared(prepared).await?)
115    }
116
117    /// Return a retained adapter record. The default is the physical model's
118    /// serialized snapshot; initialized stores apply their declared output policy.
119    async fn get_user_by_id_record(&self, id: &str) -> AuthResult<Option<AdapterRecord<S::User>>> {
120        self.get_user_by_id(id)
121            .await?
122            .map(AdapterRecord::physical)
123            .transpose()
124    }
125
126    /// Return a retained adapter record. The default is the physical model's
127    /// serialized snapshot; initialized stores apply their declared output policy.
128    async fn create_account_record(
129        &self,
130        create_account: CreateAccount,
131    ) -> AuthResult<AdapterRecord<S::Account>> {
132        AdapterRecord::physical(self.create_account(create_account).await?)
133    }
134
135    /// Return a retained adapter record. The default is the physical model's
136    /// serialized snapshot; initialized stores apply their declared output policy.
137    async fn create_session_record(
138        &self,
139        create_session: CreateSession,
140    ) -> AuthResult<AdapterRecord<S::Session>> {
141        AdapterRecord::physical(self.create_session(create_session).await?)
142    }
143
144    /// Return a retained adapter record. The default is the physical model's
145    /// serialized snapshot; initialized stores apply their declared output policy.
146    async fn update_session_active_organization_record(
147        &self,
148        token: &str,
149        organization_id: Option<&str>,
150    ) -> AuthResult<AdapterRecord<S::Session>> {
151        AdapterRecord::physical(
152            self.update_session_active_organization(token, organization_id)
153                .await?,
154        )
155    }
156
157    /// Return a retained adapter record. The default is the physical model's
158    /// serialized snapshot; initialized stores apply their declared output policy.
159    async fn update_session_active_team_record(
160        &self,
161        token: &str,
162        team_id: Option<&str>,
163    ) -> AuthResult<AdapterRecord<S::Session>> {
164        AdapterRecord::physical(self.update_session_active_team(token, team_id).await?)
165    }
166
167    /// Create through before hooks, optional physical persistence, secondary
168    /// publication, then after hooks. Unsupported adapters must fail closed.
169    async fn create_verification_record(
170        &self,
171        _data: VerificationCreation,
172        _publication: VerificationPublication,
173    ) -> AuthResult<Option<VerificationSnapshot>> {
174        Err(AuthError::NotImplemented(
175            "Verification publication phases are not supported by this store".into(),
176        ))
177    }
178    /// Read a team within the authorized organization through this transaction.
179    async fn get_team(&self, _organization_id: &str, _team_id: &str) -> AuthResult<Option<Team>> {
180        Err(AuthError::NotImplemented(
181            "Team lookup in a transaction is not supported by this store".into(),
182        ))
183    }
184    /// Add an idempotent team membership under the same transaction's capacity check.
185    async fn add_team_member(
186        &self,
187        _team_id: &str,
188        _user_id: &str,
189        _maximum: Option<f64>,
190    ) -> AuthResult<AddTeamMemberResult> {
191        Err(AuthError::NotImplemented(
192            "Team admission in a transaction is not supported by this store".into(),
193        ))
194    }
195    async fn create_member(&self, _member: CreateMember) -> AuthResult<Member> {
196        Err(AuthError::NotImplemented(
197            "Member creation in a transaction is not supported by this store".into(),
198        ))
199    }
200    /// Update the already authenticated token through the transaction connection.
201    async fn update_session_active_team(
202        &self,
203        _token: &str,
204        _team_id: Option<&str>,
205    ) -> AuthResult<S::Session> {
206        Err(AuthError::NotImplemented(
207            "Active team updates in a transaction are not supported by this store".into(),
208        ))
209    }
210    async fn update_session_active_organization(
211        &self,
212        _token: &str,
213        _organization_id: Option<&str>,
214    ) -> AuthResult<S::Session> {
215        Err(AuthError::NotImplemented(
216            "Active organization updates in a transaction are not supported by this store".into(),
217        ))
218    }
219    /// Read an existing registration owner through this transaction's connection.
220    async fn get_user_by_id(&self, _id: &str) -> AuthResult<Option<S::User>> {
221        Err(AuthError::NotImplemented(
222            "User lookup in a transaction is not supported by this store".to_owned(),
223        ))
224    }
225    /// Persist a verified passkey in the same transaction as its new session.
226    async fn create_passkey(&self, _passkey: CreatePasskey) -> AuthResult<Passkey> {
227        Err(AuthError::NotImplemented(
228            "Passkey creation in a transaction is not supported by this store".to_owned(),
229        ))
230    }
231    async fn create_user(&self, create_user: CreateUser) -> AuthResult<S::User>;
232    /// Create with endpoint-owned identity provenance. The finalized instance
233    /// applies its policy before adapter hooks. Raw adapters retain ordinary creation.
234    async fn create_user_with_source(
235        &self,
236        create_user: CreateUser,
237        _source: UserValidationSource,
238    ) -> AuthResult<S::User> {
239        self.create_user(create_user).await
240    }
241    /// Persist an admitted candidate without a second email normalization.
242    /// Custom adapters serving validation-enabled instances must implement this.
243    async fn create_user_prepared(&self, _prepared: PreparedUserCreation) -> AuthResult<S::User> {
244        Err(AuthError::NotImplemented(
245            "Prepared user creation is not supported by this transaction".into(),
246        ))
247    }
248    async fn create_account(&self, create_account: CreateAccount) -> AuthResult<S::Account>;
249    async fn create_session(&self, create_session: CreateSession) -> AuthResult<S::Session>;
250    async fn create_verification(
251        &self,
252        _verification: CreateVerification,
253    ) -> AuthResult<S::Verification> {
254        Err(AuthError::NotImplemented(
255            "Verification creation in a transaction is not supported by this store".to_owned(),
256        ))
257    }
258}
259
260#[async_trait]
261pub trait TransactionStore<S: AuthSchema>: Send + Sync {
262    async fn transaction_boxed(
263        &self,
264        work: Box<TransactionWork<S>>,
265    ) -> AuthResult<BoxedTransactionValue>;
266}
267
268/// # Errors
269///
270/// Propagates transaction or callback errors; also rejects an unexpected transaction result type.
271pub async fn transaction<S, T, F>(store: &dyn AuthStore<S>, work: F) -> AuthResult<T>
272where
273    S: AuthSchema,
274    T: Send + 'static,
275    F: for<'tx> FnOnce(&'tx dyn AuthTransaction<S>) -> TypedTransactionFuture<'tx, T>
276        + Send
277        + 'static,
278{
279    let value = store
280        .transaction_boxed(Box::new(move |tx| {
281            Box::pin(async move {
282                let result: BoxedTransactionValue = Box::new(work(tx).await?);
283                Ok(result)
284            })
285        }))
286        .await?;
287
288    value
289        .downcast::<T>()
290        .map(|boxed| *boxed)
291        .map_err(|_| AuthError::internal("store returned an invalid transaction payload"))
292}