Skip to main content

alibi_core/store/contracts/
transaction.rs

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