Skip to main content

alibi_core/store/contracts/
verification.rs

1use crate::verification::{VerificationCreation, VerificationPublication, VerificationSnapshot};
2use crate::{AuthError, AuthResult, AuthSchema, CreateVerification};
3use async_trait::async_trait;
4#[async_trait]
5pub trait VerificationStore<S: AuthSchema>: Send + Sync {
6    /// Create through before hooks, optional physical persistence, secondary
7    /// publication, then after hooks. Unsupported adapters must fail closed.
8    async fn create_verification_record(
9        &self,
10        _data: VerificationCreation,
11        _publication: VerificationPublication,
12    ) -> AuthResult<Option<VerificationSnapshot>> {
13        Err(AuthError::NotImplemented(
14            "Verification publication phases are not supported by this store".into(),
15        ))
16    }
17    /// Atomically remove the newest generation and all siblings, returning the
18    /// actual winning snapshot even when expired. This keeps legacy fallback
19    /// from resurrecting an older logical generation.
20    async fn consume_verification_snapshot(
21        &self,
22        _identifier: &str,
23    ) -> AuthResult<Option<S::Verification>> {
24        Err(AuthError::NotImplemented(
25            "Raw atomic verification consumption is not supported by this store".into(),
26        ))
27    }
28    /// Update every physical match, with one actual adapter result snapshot.
29    async fn update_verification_by_identifier(
30        &self,
31        _identifier: &str,
32        _data: crate::UpdateVerification,
33    ) -> AuthResult<Option<VerificationSnapshot>> {
34        Err(AuthError::NotImplemented(
35            "Verification identifier updates are not supported by this store".into(),
36        ))
37    }
38    /// Reserve by the original logical identifier while storing its configured
39    /// transformed identifier. Return the actual newly inserted model only.
40    async fn reserve_verification_record(
41        &self,
42        _logical_identifier: &str,
43        _data: CreateVerification,
44    ) -> AuthResult<Option<S::Verification>> {
45        Err(AuthError::NotImplemented(
46            "Configured verification reservation is not supported by this store".into(),
47        ))
48    }
49    async fn create_verification(
50        &self,
51        verification: CreateVerification,
52    ) -> AuthResult<S::Verification>;
53    async fn get_verification(
54        &self,
55        identifier: &str,
56        value: &str,
57    ) -> AuthResult<Option<S::Verification>>;
58    async fn get_verification_by_value(&self, value: &str) -> AuthResult<Option<S::Verification>>;
59    async fn get_verification_by_identifier(
60        &self,
61        identifier: &str,
62    ) -> AuthResult<Option<S::Verification>>;
63    async fn consume_verification(
64        &self,
65        identifier: &str,
66        value: &str,
67    ) -> AuthResult<Option<S::Verification>>;
68    /// Fetch the newest generation, including expired records. The caller
69    /// decides whether cleanup and a distinct expiry error are required.
70    async fn get_latest_verification_by_identifier(
71        &self,
72        _identifier: &str,
73    ) -> AuthResult<Option<S::Verification>> {
74        Err(AuthError::internal(
75            "raw verification lookup is not supported by this store",
76        ))
77    }
78    /// Atomically invalidate an identifier and return its newest generation.
79    /// Exactly one concurrent caller can receive a row. Expired records are
80    /// removed along with every sibling and return `None`.
81    async fn consume_verification_by_identifier(
82        &self,
83        _identifier: &str,
84    ) -> AuthResult<Option<S::Verification>> {
85        Err(AuthError::internal(
86            "atomic verification consumption is not supported by this store",
87        ))
88    }
89    async fn delete_verifications_by_identifier(&self, _identifier: &str) -> AuthResult<()> {
90        Err(AuthError::internal(
91            "verification invalidation is not supported by this store",
92        ))
93    }
94    /// Update a generation only when its value still matches the snapshot.
95    async fn compare_and_swap_verification(
96        &self,
97        _id: &str,
98        _expected_value: &str,
99        _value: &str,
100        _expires_at: chrono::DateTime<chrono::Utc>,
101    ) -> AuthResult<bool> {
102        Err(AuthError::internal(
103            "atomic verification updates are not supported by this store",
104        ))
105    }
106    /// Insert a deterministic reservation exactly once. An expired marker
107    /// remains reserved until it is cleaned up or explicitly consumed.
108    async fn reserve_verification(&self, _verification: CreateVerification) -> AuthResult<bool> {
109        Err(AuthError::internal(
110            "verification reservation is not supported by this store",
111        ))
112    }
113    async fn delete_verification(&self, id: &str) -> AuthResult<()>;
114    async fn delete_expired_verifications(&self) -> AuthResult<usize>;
115}
116
117/// Upstream's deterministic database key for first-writer verification claims.
118#[must_use]
119pub fn verification_reservation_key(identifier: &str) -> (String, [u8; 32]) {
120    use base64::Engine;
121    use sha2::{Digest, Sha256};
122
123    let mut hash = Sha256::new();
124    hash.update(b"reserve:");
125    hash.update(identifier.as_bytes());
126    let digest: [u8; 32] = hash.finalize().into();
127    (
128        base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(digest),
129        digest,
130    )
131}
132
133/// The reservation key under the configured ID strategy, as upstream's
134/// `encodeDeterministicId` (1.7.12) encodes it. Under `generateId: "uuid"`
135/// the first 16 digest bytes become a version 5, RFC 9562 variant UUID so
136/// the forced ID survives; other strategies keep the base64url digest.
137#[must_use]
138pub fn verification_reservation_id(
139    identifier: &str,
140    database: &crate::config::AdvancedDatabaseConfig,
141) -> (String, [u8; 32]) {
142    let (encoded, digest) = verification_reservation_key(identifier);
143    if !matches!(
144        database.generate_id,
145        Some(crate::config::DatabaseIdStrategy::Uuid)
146    ) {
147        return (encoded, digest);
148    }
149    let mut bytes = [0_u8; 16];
150    bytes.copy_from_slice(&digest[..16]);
151    bytes[6] = (bytes[6] & 0x0f) | 0x50;
152    bytes[8] = (bytes[8] & 0x3f) | 0x80;
153    (uuid::Uuid::from_bytes(bytes).to_string(), digest)
154}
155
156// LCOV_EXCL_START
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    // Upstream reference: packages/core/src/db/adapter/get-id-field.test.ts :: encodeDeterministicId (1.7.12).
162    #[test]
163    fn uuid_strategy_reserves_a_version_5_uuid_from_the_digest() {
164        let mut database = crate::config::AdvancedDatabaseConfig::default();
165        let (plain, digest) = verification_reservation_id("siwe-email-claim-a@b.c", &database);
166        assert_eq!(
167            (plain.clone(), digest),
168            verification_reservation_key("siwe-email-claim-a@b.c")
169        );
170        database.generate_id = Some(crate::config::DatabaseIdStrategy::Uuid);
171        let (id, same) = verification_reservation_id("siwe-email-claim-a@b.c", &database);
172        assert_eq!(same, digest);
173        let parsed = uuid::Uuid::parse_str(&id).unwrap();
174        assert_eq!(parsed.get_version_num(), 5);
175        assert_eq!(parsed.get_variant(), uuid::Variant::RFC4122);
176        assert_eq!(parsed.as_bytes()[..6], digest[..6]);
177        assert_ne!(id, plain);
178    }
179}
180// LCOV_EXCL_STOP